Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.usercentrics.reactnative

import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.usercentrics.sdk.UsercentricsDisposableEvent
import com.usercentrics.sdk.UsercentricsEvent
import com.usercentrics.reactnative.api.UsercentricsProxy
Expand Down Expand Up @@ -44,7 +43,13 @@ internal class RNUsercentricsModule(
val bannerSettings = options?.bannerSettingsFromMap(context)

val activity = reactContextProvider.activity()!!
usercentricsProxy.showFirstLayer(activity, bannerSettings, promise)
usercentricsProxy.showFirstLayer(
activity,
bannerSettings,
onLoginClicked = { url -> emitEvent(ON_LOGIN_CLICKED_EVENT, url) },
onSubscribeClicked = { url -> emitEvent(ON_SUBSCRIBE_CLICKED_EVENT, url) },
promise,
Comment thread
islameldesoky95 marked this conversation as resolved.
Resolved
)
} catch (e: Exception) {
promise.reject(e)
}
Expand Down Expand Up @@ -254,6 +259,24 @@ internal class RNUsercentricsModule(
})
}

@ReactMethod
override fun notifyLoginSuccess(promise: Promise) {
usercentricsProxy.instance.notifyLoginSuccess({
promise.resolve(null)
}, {
promise.reject(it)
})
}

@ReactMethod
override fun notifySubscribeSuccess(promise: Promise) {
usercentricsProxy.instance.notifySubscribeSuccess({
promise.resolve(null)
}, {
promise.reject(it)
})
}

@ReactMethod
override fun addListener(eventName: String) {
if (eventName != ON_GPP_SECTION_CHANGE_EVENT) return
Expand Down Expand Up @@ -282,10 +305,8 @@ internal class RNUsercentricsModule(
super.invalidate()
}

private fun emitEvent(eventName: String, payload: WritableMap) {
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(eventName, payload)
private fun emitEvent(eventName: String, payload: Any?) {
reactApplicationContext.emitDeviceEvent(eventName, payload)
}

private fun readableMapValueToAny(map: ReadableMap): Any? {
Expand Down Expand Up @@ -322,5 +343,7 @@ internal class RNUsercentricsModule(
companion object {
const val NAME = "RNUsercentricsModule"
const val ON_GPP_SECTION_CHANGE_EVENT = "onGppSectionChange"
const val ON_LOGIN_CLICKED_EVENT = "onLoginClicked"
const val ON_SUBSCRIBE_CLICKED_EVENT = "onSubscribeClicked"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ abstract class RNUsercentricsModuleSpec internal constructor(context: ReactAppli
@ReactMethod
abstract fun clearUserSession(promise: Promise)

@ReactMethod
abstract fun notifyLoginSuccess(promise: Promise)

@ReactMethod
abstract fun notifySubscribeSuccess(promise: Promise)

@ReactMethod
abstract fun getConsents(promise: Promise)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ interface UsercentricsProxy {
fun initialize(context: Context, options: UsercentricsOptions)
fun isReady(onSuccess: (UsercentricsReadyStatus) -> Unit, onFailure: (UsercentricsError) -> Unit)

fun showFirstLayer(activity: Activity, bannerSettings: BannerSettings?, promise: Promise)
fun showFirstLayer(
activity: Activity,
bannerSettings: BannerSettings?,
onLoginClicked: (String?) -> Unit,
onSubscribeClicked: (String?) -> Unit,
promise: Promise,
)
Comment thread
islameldesoky95 marked this conversation as resolved.
Resolved
Comment thread
islameldesoky95 marked this conversation as resolved.
Resolved
fun showSecondLayer(activity: Activity, bannerSettings: BannerSettings?, promise: Promise)
}

Expand All @@ -39,11 +45,15 @@ internal class UsercentricsProxyImpl : UsercentricsProxy {
override fun showFirstLayer(
activity: Activity,
bannerSettings: BannerSettings?,
onLoginClicked: (String?) -> Unit,
onSubscribeClicked: (String?) -> Unit,
promise: Promise,
) {
UsercentricsBanner(activity, bannerSettings).showFirstLayer {
promise.resolve(it?.toWritableMap())
}
UsercentricsBanner(activity, bannerSettings).showFirstLayer(
callback = { promise.resolve(it?.toWritableMap()) },
onLoginClicked = onLoginClicked,
onSubscribeClicked = onSubscribeClicked,
)
}

override fun showSecondLayer(
Expand Down
19 changes: 18 additions & 1 deletion ios/Manager/UsercentricsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ public protocol UsercentricsManager {
func restoreUserSession(controllerId: String, onSuccess: @escaping ((UsercentricsReadyStatus) -> Void), onFailure: @escaping ((Error) -> Void))

func showFirstLayer(bannerSettings: BannerSettings?,
onLoginClicked: @escaping (String?) -> Void,
onSubscribeClicked: @escaping (String?) -> Void,
dismissViewHandler: @escaping (UsercentricsConsentUserResponse) -> Void)

func notifyLoginSuccess(onSuccess: @escaping (() -> Void), onError: @escaping ((Error) -> Void))
func notifySubscribeSuccess(onSuccess: @escaping (() -> Void), onError: @escaping ((Error) -> Void))

func showSecondLayer(bannerSettings: BannerSettings?,
dismissViewHandler: @escaping (UsercentricsConsentUserResponse) -> Void)

Expand Down Expand Up @@ -60,8 +65,20 @@ final class UsercentricsManagerImplementation: UsercentricsManager {
}

func showFirstLayer(bannerSettings: BannerSettings?,
onLoginClicked: @escaping (String?) -> Void,
onSubscribeClicked: @escaping (String?) -> Void,
dismissViewHandler: @escaping (UsercentricsConsentUserResponse) -> Void) {
UsercentricsBanner(bannerSettings: bannerSettings).showFirstLayer(completionHandler: dismissViewHandler)
UsercentricsBanner(bannerSettings: bannerSettings).showFirstLayer(onLoginClicked: onLoginClicked,
onSubscribeClicked: onSubscribeClicked,
completionHandler: dismissViewHandler)
}

func notifyLoginSuccess(onSuccess: @escaping (() -> Void), onError: @escaping ((Error) -> Void)) {
UsercentricsCore.shared.notifyLoginSuccess(onSuccess: onSuccess, onError: onError)
}

func notifySubscribeSuccess(onSuccess: @escaping (() -> Void), onError: @escaping ((Error) -> Void)) {
UsercentricsCore.shared.notifySubscribeSuccess(onSuccess: onSuccess, onError: onError)
}

func showSecondLayer(bannerSettings: BannerSettings?,
Expand Down
6 changes: 6 additions & 0 deletions ios/RNUsercentricsModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,10 @@ @interface RCT_EXTERN_MODULE(RNUsercentricsModule, NSObject)

RCT_EXTERN_METHOD(clearUserSession:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(notifyLoginSuccess:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(notifySubscribeSuccess:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
@end
26 changes: 24 additions & 2 deletions ios/RNUsercentricsModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class RNUsercentricsModule: RCTEventEmitter {
}

override func supportedEvents() -> [String]! {
return [Self.onGppSectionChangeEvent]
return [Self.onGppSectionChangeEvent, Self.onLoginClickedEvent, Self.onSubscribeClickedEvent]
}

override func startObserving() {
Expand Down Expand Up @@ -79,7 +79,11 @@ class RNUsercentricsModule: RCTEventEmitter {
return
}

self.usercentricsManager.showFirstLayer(bannerSettings: BannerSettings(from: dict)) { response in
self.usercentricsManager.showFirstLayer(bannerSettings: BannerSettings(from: dict), onLoginClicked: { [weak self] url in
self?.sendEvent(withName: Self.onLoginClickedEvent, body: url)
}, onSubscribeClicked: { [weak self] url in
self?.sendEvent(withName: Self.onSubscribeClickedEvent, body: url)
}) { response in
resolve(response.toDictionary())
}
}
Expand Down Expand Up @@ -270,7 +274,25 @@ class RNUsercentricsModule: RCTEventEmitter {
}
}

@objc func notifyLoginSuccess(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
usercentricsManager.notifyLoginSuccess {
resolve(nil)
} onError: { error in
reject("usercentrics_reactNative_notifyLoginSuccess_error", error.localizedDescription, error)
}
}

@objc func notifySubscribeSuccess(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
usercentricsManager.notifySubscribeSuccess {
resolve(nil)
} onError: { error in
reject("usercentrics_reactNative_notifySubscribeSuccess_error", error.localizedDescription, error)
}
}

private static let onGppSectionChangeEvent = "onGppSectionChange"
private static let onLoginClickedEvent = "onLoginClicked"
private static let onSubscribeClickedEvent = "onSubscribeClicked"
}

// MARK: - RCTBridgeModule & TurboModule Conformance
Expand Down
7 changes: 7 additions & 0 deletions ios/RNUsercentricsModuleSpec.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ NS_ASSUME_NONNULL_BEGIN
- (void)clearUserSession:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

// Consent or Pay
- (void)notifyLoginSuccess:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

- (void)notifySubscribeSuccess:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

Comment thread
islameldesoky95 marked this conversation as resolved.
Resolved
// Data Retrieval
- (void)getConsents:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;
Expand Down
28 changes: 28 additions & 0 deletions sample/ios/sampleTests/Fake/FakeUsercentricsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,10 +216,20 @@ final class FakeUsercentricsManager: UsercentricsManager {
}

var showFirstLayerBannerSettings: BannerSettings?
var loginClickedUrl: String?
var subscribeClickedUrl: String?

func showFirstLayer(bannerSettings: BannerSettings?,
onLoginClicked: @escaping (String?) -> Void,
onSubscribeClicked: @escaping (String?) -> Void,
dismissViewHandler: @escaping (UsercentricsConsentUserResponse) -> Void) {
self.showFirstLayerBannerSettings = bannerSettings
if let loginClickedUrl = loginClickedUrl {
onLoginClicked(loginClickedUrl)
}
if let subscribeClickedUrl = subscribeClickedUrl {
onSubscribeClicked(subscribeClickedUrl)
}
dismissViewHandler(UsercentricsConsentUserResponse(consents: [], controllerId: "", userInteraction: .acceptAll))
Comment thread
islameldesoky95 marked this conversation as resolved.
Resolved
}

Expand All @@ -244,4 +254,22 @@ final class FakeUsercentricsManager: UsercentricsManager {
onError(clearUserSessionError)
}
}

var notifyLoginSuccessError: Error?
func notifyLoginSuccess(onSuccess: @escaping (() -> Void), onError: @escaping ((Error) -> Void)) {
if let notifyLoginSuccessError = notifyLoginSuccessError {
onError(notifyLoginSuccessError)
return
}
onSuccess()
}

var notifySubscribeSuccessError: Error?
func notifySubscribeSuccess(onSuccess: @escaping (() -> Void), onError: @escaping ((Error) -> Void)) {
if let notifySubscribeSuccessError = notifySubscribeSuccessError {
onError(notifySubscribeSuccessError)
return
}
onSuccess()
}
}
18 changes: 18 additions & 0 deletions sample/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ config.resolver.extraNodeModules = {
"@usercentrics/react-native-sdk": path.resolve(__dirname, "../"),
};

// Force a single react-native copy — the SDK's own node_modules/react-native (0.79.7) is a
// separate install from the sample's (0.81.4), which was creating two disconnected
// RCTDeviceEventEmitter singletons: native events emitted through the SDK's copy never
// reached listeners registered through the sample app's copy. extraNodeModules alone doesn't
// work here since it's only a fallback consulted when normal resolution fails — react-native
// resolves fine in both locations, so we need to intercept resolution directly.
const sampleReactNative = path.resolve(__dirname, "node_modules/react-native");
config.resolver.resolveRequest = (context, moduleName, platform) => {
if (moduleName === "react-native" || moduleName.startsWith("react-native/")) {
return context.resolveRequest(
context,
path.join(sampleReactNative, moduleName.slice("react-native".length)),
platform
);
}
return context.resolveRequest(context, moduleName, platform);
};

// Tell Metro where to resolve modules from — needed so that files inside
// the SDK's node_modules can resolve their own transitive dependencies.
config.resolver.nodeModulesPaths = [
Expand Down
33 changes: 32 additions & 1 deletion sample/src/screens/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Button, StyleSheet, View } from 'react-native';
import { Alert, Button, StyleSheet, View } from 'react-native';
import {
BannerSettings,
Usercentrics,
Expand Down Expand Up @@ -42,6 +42,37 @@ export const HomeScreen = ({ navigation }: { navigation: any }) => {
.catch(e => console.error('[Usercentrics] status failed:', e));
}, [showFirstLayer]);

React.useEffect(() => {
const loginSubscription = Usercentrics.onLoginClicked(async (url) => {
console.log('[Usercentrics] onLoginClicked:', url);
Alert.alert('onLoginClicked', `url: ${url}`);
try {
await Usercentrics.notifyLoginSuccess();
Comment thread
islameldesoky95 marked this conversation as resolved.
Resolved
Comment thread
islameldesoky95 marked this conversation as resolved.
Resolved
console.log('[Usercentrics] notifyLoginSuccess done');
Alert.alert('notifyLoginSuccess', 'TCF storage cleared');
Comment thread
islameldesoky95 marked this conversation as resolved.
Resolved
} catch (e) {
console.error('[Usercentrics] notifyLoginSuccess failed:', e);
Alert.alert('notifyLoginSuccess failed', String(e));
}
});
const subscribeSubscription = Usercentrics.onSubscribeClicked(async (url) => {
console.log('[Usercentrics] onSubscribeClicked:', url);
Alert.alert('onSubscribeClicked', `url: ${url}`);
try {
await Usercentrics.notifySubscribeSuccess();
console.log('[Usercentrics] notifySubscribeSuccess done');
Alert.alert('notifySubscribeSuccess', 'TCF storage cleared');
} catch (e) {
console.error('[Usercentrics] notifySubscribeSuccess failed:', e);
Alert.alert('notifySubscribeSuccess failed', String(e));
}
});
return () => {
loginSubscription.remove();
subscribeSubscription.remove();
};
}, []);
Comment thread
islameldesoky95 marked this conversation as resolved.
Resolved

async function showSecondLayer() {
try {
const response = await Usercentrics.showSecondLayer({
Expand Down
4 changes: 4 additions & 0 deletions src/NativeUsercentrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export interface Spec extends TurboModule {
getControllerId(): Promise<string>;
clearUserSession(): Promise<UsercentricsReadyStatus>;

// Consent or Pay
notifyLoginSuccess(): Promise<void>;
notifySubscribeSuccess(): Promise<void>;

// Data Retrieval
getConsents(): Promise<Array<UsercentricsServiceConsent>>;
getCMPData(): Promise<UsercentricsCMPData>;
Expand Down
26 changes: 26 additions & 0 deletions src/Usercentrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,30 @@ export const Usercentrics = {
onGppSectionChange: (callback: (payload: GppSectionChangePayload) => void): EmitterSubscription => {
return eventEmitter.addListener("onGppSectionChange", callback);
},

// Fires when the user taps the Consent-or-Pay 1st-layer subscriber-login link. The banner is not
// dismissed automatically — call notifyLoginSuccess once the host app confirms login, then dismiss
// the banner yourself.
Comment thread
islameldesoky95 marked this conversation as resolved.
Resolved
onLoginClicked: (callback: (url: string | null) => void): EmitterSubscription => {
return eventEmitter.addListener("onLoginClicked", callback);
},
Comment thread
islameldesoky95 marked this conversation as resolved.
Resolved
Comment thread
islameldesoky95 marked this conversation as resolved.
Resolved

// Fires when the user taps the Consent-or-Pay 1st-layer Reject & Subscribe button. The banner is not
// dismissed automatically — call notifySubscribeSuccess once the host app confirms the subscription,
// then dismiss the banner yourself.
onSubscribeClicked: (callback: (url: string | null) => void): EmitterSubscription => {
return eventEmitter.addListener("onSubscribeClicked", callback);
},
Comment thread
islameldesoky95 marked this conversation as resolved.
Resolved

// Clears stored TCF consent data after a successful Consent-or-Pay login.
notifyLoginSuccess: async (): Promise<void> => {
await RNUsercentricsModule.isReady();
return RNUsercentricsModule.notifyLoginSuccess();
},

// Clears stored TCF consent data after a successful Consent-or-Pay subscription.
notifySubscribeSuccess: async (): Promise<void> => {
await RNUsercentricsModule.isReady();
return RNUsercentricsModule.notifySubscribeSuccess();
},
}
4 changes: 4 additions & 0 deletions src/fabric/NativeUsercentricsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export interface Spec extends TurboModule {
getControllerId(): Promise<string>;
clearUserSession(): Promise<Object>;

// Consent or Pay
notifyLoginSuccess(): Promise<void>;
notifySubscribeSuccess(): Promise<void>;

// Data Retrieval
getConsents(): Promise<Array<Object>>;
getCMPData(): Promise<Object>;
Expand Down
Loading