From ed01d9372bbab80a822fd9a7c16ac24624e7fc92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Herculano?= Date: Tue, 9 Sep 2025 18:00:49 +0200 Subject: [PATCH 1/8] move RNSourcepointAction to RNSPAction --- example/ios/Podfile.lock | 2 +- ios/RNSPAction.swift | 27 ++++++++++ ...tActionType.swift => RNSPActionType.swift} | 4 +- ios/RNSPCampaignType.swift | 43 +++++++++++++++ ios/{RNSourcepointCmp.swift => RNSPCmp.swift} | 33 ++---------- ios/RNSPError.swift | 52 +++++++++++++++++++ ios/RNSPStringifieable.swift | 27 ++++++++++ ios/ReactNativeCmp.mm | 2 +- 8 files changed, 158 insertions(+), 32 deletions(-) create mode 100644 ios/RNSPAction.swift rename ios/{RNSourcepointActionType.swift => RNSPActionType.swift} (89%) create mode 100644 ios/RNSPCampaignType.swift rename ios/{RNSourcepointCmp.swift => RNSPCmp.swift} (83%) create mode 100644 ios/RNSPError.swift create mode 100644 ios/RNSPStringifieable.swift diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 9f2e547..891bfd3 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1996,4 +1996,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: decdc7519d77aa5eae65b167fa59bcfce25e15d2 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/ios/RNSPAction.swift b/ios/RNSPAction.swift new file mode 100644 index 0000000..ee127dc --- /dev/null +++ b/ios/RNSPAction.swift @@ -0,0 +1,27 @@ +// +// RNAction.swift +// Pods +// +// Created by Andre Herculano on 5/9/25. +// + +@objcMembers public class RNSPAction: NSObject { + public let type: RNSPActionType + public let customActionId: String? + + @objc public init(type: RNSPActionType, customActionId: String?) { + self.type = type + self.customActionId = customActionId + } +} + +@objc extension RNSPAction: RNSPStringifieable { + public var stringifiedJson: String { stringifiedJson() } + public var defaultStringEncoded: String { "{\"actionType\":\"unknown\"}" } + public var dictionaryEncoded: [String: Any] { + [ + "actionType": type.description, + "customActionId": customActionId + ].compactMapValues { $0 } + } +} diff --git a/ios/RNSourcepointActionType.swift b/ios/RNSPActionType.swift similarity index 89% rename from ios/RNSourcepointActionType.swift rename to ios/RNSPActionType.swift index 16d5c78..f03c4b8 100644 --- a/ios/RNSourcepointActionType.swift +++ b/ios/RNSPActionType.swift @@ -1,5 +1,5 @@ // -// RNSourcepointActionType.swift +// RNSPActionType.swift // sourcepoint-react-native-cmp // // Created by Andre Herculano on 31/5/24. @@ -8,7 +8,7 @@ import Foundation import ConsentViewController -@objc public enum RNSourcepointActionType: Int, CustomStringConvertible { +@objc public enum RNSPActionType: Int, CustomStringConvertible { case acceptAll, rejectAll, showPrivacyManager, saveAndExit, dismiss, pmCancel, unknown public var description: String { diff --git a/ios/RNSPCampaignType.swift b/ios/RNSPCampaignType.swift new file mode 100644 index 0000000..88a5a0a --- /dev/null +++ b/ios/RNSPCampaignType.swift @@ -0,0 +1,43 @@ +// +// RNSPCampaignType.swift +// Pods +// +// Created by Andre Herculano on 5/9/25. +// + +import Foundation +import ConsentViewController + +enum RNSPCampaignType: String { + case gdpr, usnat, preferences, globalcmp, unknown + + init(_ campaignType: SPCampaignType) { + switch campaignType { + case .gdpr: self = .gdpr + case .usnat: self = .usnat + case .preferences: self = .preferences + case .globalcmp: self = .globalcmp + default: self = .unknown + } + } + + public init(rawValue: String) { + switch rawValue.lowercased() { + case "gdpr": self = .gdpr + case "usnat": self = .usnat + case "preferences": self = .preferences + case "globalcmp": self = .globalcmp + default: self = .unknown + } + } + + func toSP() -> SPCampaignType { + switch self { + case .gdpr: return .gdpr + case .usnat: return .usnat + case .preferences: return .preferences + case .globalcmp: return .globalcmp + default: return .unknown + } + } +} diff --git a/ios/RNSourcepointCmp.swift b/ios/RNSPCmp.swift similarity index 83% rename from ios/RNSourcepointCmp.swift rename to ios/RNSPCmp.swift index c74187a..e030129 100644 --- a/ios/RNSourcepointCmp.swift +++ b/ios/RNSPCmp.swift @@ -19,36 +19,13 @@ import React } } -@objcMembers public class RNAction: NSObject { - public let type: RNSourcepointActionType - public let customActionId: String? - - @objc public init(type: RNSourcepointActionType, customActionId: String?) { - self.type = type - self.customActionId = customActionId - } - - func toDictionary() -> [String: Any] { - ["actionType": type.description, "customActionId": customActionId] - } - - @objc public func stringifiedJson() -> String { - if let jsonData = try? JSONSerialization.data(withJSONObject: toDictionary()), - let jsonString = String(data: jsonData, encoding: .utf8) { - return jsonString - } else { - return "{\"actionType\":\"unknown\"}" - } - } -} - @objc public protocol ReactNativeCmpImplDelegate { - func onAction(_ action: RNAction) + func onAction(_ action: RNSPAction) func onSPUIReady() func onSPUIFinished() func onFinished() func onMessageInactivityTimeout() - func onError(description: String) + func onError(_ error: RNSPError) } @objcMembers public class ReactNativeCmpImpl: NSObject { @@ -110,7 +87,7 @@ import React } public func rejectAll(_ campaignType: String) { - consentManager?.rejectAll(campaignType: SPCampaignType(rawValue: campaignType == "gdpr" ? "GDPR" : campaignType )) + consentManager?.rejectAll(campaignType: RNSPCampaignType(rawValue: campaignType).toSP()) } weak var rootViewController: UIViewController? { @@ -139,7 +116,7 @@ import React } public func onAction(_ action: SPAction, from controller: UIViewController) { - delegate?.onAction(RNAction(type: RNSourcepointActionType(from: action.type), customActionId: action.customActionId)) + delegate?.onAction(RNSPAction(type: RNSPActionType(from: action.type), customActionId: action.customActionId)) } public func onSPUIReady(_ controller: UIViewController) { @@ -163,7 +140,7 @@ import React public func onError(error: SPError) { print("Something went wrong", error) - delegate?.onError(description: error.description) + delegate?.onError(RNSPError(error)) } public func dismissMessage() { diff --git a/ios/RNSPError.swift b/ios/RNSPError.swift new file mode 100644 index 0000000..d1c21dc --- /dev/null +++ b/ios/RNSPError.swift @@ -0,0 +1,52 @@ +// +// RNError.swift +// Pods +// +// Created by Andre Herculano on 5/9/25. +// + +import ConsentViewController + +public enum RNSPErrorName: String { + case unknown = "Unknown" + + init(_ error: SPError) { + switch error { + default: + self = .unknown + } + } +} + + +@objcMembers public class RNSPError: NSObject { + public let name: RNSPErrorName + public let errorDescription: String + public let campaignType: SPCampaignType? + + init (name: RNSPErrorName, description: String, campaignType: SPCampaignType?) { + self.name = name + self.errorDescription = description + self.campaignType = campaignType + } + + convenience init(_ error: SPError) { + self.init( + name: RNSPErrorName(error), + description: error.description, + campaignType: error.campaignType + ) + } +} + +@objc extension RNSPError: RNSPStringifieable { + public var stringifiedJson: String { stringifiedJson() } + public var defaultStringEncoded: String { "{\"name\":\"Unknown\",\"description\":\"unknown error\"}" } + public var dictionaryEncoded: [String: Any] { + [ + "name": name.rawValue, + "description": errorDescription, + "campaignType": campaignType?.rawValue + ].compactMapValues { $0 } + } +} diff --git a/ios/RNSPStringifieable.swift b/ios/RNSPStringifieable.swift new file mode 100644 index 0000000..f266073 --- /dev/null +++ b/ios/RNSPStringifieable.swift @@ -0,0 +1,27 @@ +// +// RNSPStringifieable.swift +// Pods +// +// Created by Andre Herculano on 5/9/25. +// + +import ConsentViewController + +@objc public protocol RNSPStringifieable { + @objc var defaultStringEncoded: String { get } + @objc var dictionaryEncoded: [String: Any] { get } + @objc var stringifiedJson: String { get } + + @objc optional func stringifiedJson(default: String) -> String +} + +public extension RNSPStringifieable { + func stringifiedJson() -> String { + if let jsonData = try? JSONSerialization.data(withJSONObject: dictionaryEncoded), + let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString + } else { + return defaultStringEncoded + } + } +} diff --git a/ios/ReactNativeCmp.mm b/ios/ReactNativeCmp.mm index 9e99601..700faf1 100644 --- a/ios/ReactNativeCmp.mm +++ b/ios/ReactNativeCmp.mm @@ -133,7 +133,7 @@ - (void)rejectAll:(nonnull NSString *)campaignType { } // MARK: SPDelegate -- (void)onAction:(RNAction*)action { +- (void)onAction:(RNSPAction*)action { [self emitInternalOnAction: [action stringifiedJson]]; } From 18827e3b63fdacf47e9c86cab74a0e433b52ef38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Herculano?= Date: Tue, 9 Sep 2025 18:01:30 +0200 Subject: [PATCH 2/8] change onError to receive an SPError object --- ios/ReactNativeCmp.mm | 7 ++----- src/NativeReactNativeCmp.ts | 5 +++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ios/ReactNativeCmp.mm b/ios/ReactNativeCmp.mm index 700faf1..8959fd9 100644 --- a/ios/ReactNativeCmp.mm +++ b/ios/ReactNativeCmp.mm @@ -137,11 +137,8 @@ - (void)onAction:(RNSPAction*)action { [self emitInternalOnAction: [action stringifiedJson]]; } -- (void)onErrorWithDescription:(NSString * _Nonnull)description { - NSDictionary *dict = @{@"description": description}; - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil]; - NSString *json = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; - [self emitInternalOnError: json]; +- (void)onError:(RNSPError*)error { + [self emitInternalOnError: [error stringifiedJson]]; } - (void)onFinished { diff --git a/src/NativeReactNativeCmp.ts b/src/NativeReactNativeCmp.ts index a2b821d..3ea041a 100644 --- a/src/NativeReactNativeCmp.ts +++ b/src/NativeReactNativeCmp.ts @@ -209,8 +209,13 @@ export type SPBuildOptions = { androidDismissMessageOnBackPress?: boolean; } +export type SPErrorName = + | "Unknown"; + export type SPError = { + name: SPErrorName; description: string; + campaignType?: SPCampaignType; }; export interface Spec extends TurboModule { From bd355cefbfb7dd66c389f38d81e1125e2f7d6f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Herculano?= Date: Wed, 10 Sep 2025 15:06:54 +0200 Subject: [PATCH 3/8] android: remove consent view in case of an error --- .../sourcepoint/reactnativecmp/ReactNativeCmpModule.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/sourcepoint/reactnativecmp/ReactNativeCmpModule.kt b/android/src/main/java/com/sourcepoint/reactnativecmp/ReactNativeCmpModule.kt index 0a256a6..0dc8a90 100644 --- a/android/src/main/java/com/sourcepoint/reactnativecmp/ReactNativeCmpModule.kt +++ b/android/src/main/java/com/sourcepoint/reactnativecmp/ReactNativeCmpModule.kt @@ -30,6 +30,7 @@ data class SPLoadMessageParams(val authId: String?) { class ReactNativeCmpModule(reactContext: ReactApplicationContext) : NativeReactNativeCmpSpec(reactContext), SpClient { private var spConsentLib: SpConsentLib? = null + private var consentView: View? = null override fun getName() = NAME @@ -179,7 +180,11 @@ class ReactNativeCmpModule(reactContext: ReactApplicationContext) : NativeReactN override fun onConsentReady(consent: SPConsents) {} override fun onError(error: Throwable) { - emitInternalOnError(Json.encodeToString(mapOf("description" to error.message))) + emitInternalOnError(RNSPError.from(error).toJsonString()) + consentView?.let { + spConsentLib?.removeView(it) + } + consentView = null } override fun onNoIntentActivitiesFound(url: String) {} @@ -193,11 +198,13 @@ class ReactNativeCmpModule(reactContext: ReactApplicationContext) : NativeReactN } override fun onUIFinished(view: View) { + consentView = null spConsentLib?.removeView(view) emitOnSPUIFinished() } override fun onUIReady(view: View) { + consentView = view spConsentLib?.showView(view) emitOnSPUIReady() } From 6e3dcd705f87315365f830ee7074178a673dc8a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Herculano?= Date: Wed, 10 Sep 2025 15:09:25 +0200 Subject: [PATCH 4/8] android: implement RNSPError --- .../sourcepoint/reactnativecmp/RNSPError.kt | 56 +++++++++++++++++++ src/NativeReactNativeCmp.ts | 9 ++- 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 android/src/main/java/com/sourcepoint/reactnativecmp/RNSPError.kt diff --git a/android/src/main/java/com/sourcepoint/reactnativecmp/RNSPError.kt b/android/src/main/java/com/sourcepoint/reactnativecmp/RNSPError.kt new file mode 100644 index 0000000..879148a --- /dev/null +++ b/android/src/main/java/com/sourcepoint/reactnativecmp/RNSPError.kt @@ -0,0 +1,56 @@ +package com.sourcepoint.reactnativecmp + +import com.sourcepoint.cmplibrary.data.network.util.CampaignType +import com.sourcepoint.cmplibrary.exception.ConsentLibExceptionK +import com.sourcepoint.cmplibrary.exception.FailedToDeleteCustomConsent +import com.sourcepoint.cmplibrary.exception.FailedToLoadMessages +import com.sourcepoint.cmplibrary.exception.FailedToPostCustomConsent +import com.sourcepoint.cmplibrary.exception.NoIntentFoundForUrl +import com.sourcepoint.cmplibrary.exception.NoInternetConnectionException +import com.sourcepoint.cmplibrary.exception.RenderingAppException +import com.sourcepoint.cmplibrary.exception.ReportActionException +import com.sourcepoint.cmplibrary.exception.UnableToDownloadRenderingApp +import com.sourcepoint.cmplibrary.exception.UnableToLoadRenderingApp +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +enum class RNSPErrorName { + Unknown, + NoInternetConnection, + LoadMessagesError, + RenderingAppError, + ReportActionError, + ReportCustomConsentError, + AndroidNoIntentFound; + + companion object { + fun from(throwable: Throwable) = when (throwable) { + is NoInternetConnectionException -> NoInternetConnection + is FailedToLoadMessages -> LoadMessagesError + is UnableToDownloadRenderingApp, is UnableToLoadRenderingApp, is RenderingAppException -> RenderingAppError + is ReportActionException -> ReportActionError + is FailedToPostCustomConsent, is FailedToDeleteCustomConsent -> ReportCustomConsentError + is NoIntentFoundForUrl -> AndroidNoIntentFound + else -> Unknown + } + } +} + +data class RNSPError(val name: RNSPErrorName, val description: String, val campaignType: CampaignType?) { + companion object { + fun from(throwable: Throwable) = RNSPError( + name = RNSPErrorName.from(throwable), + description = (throwable as? ConsentLibExceptionK)?.description ?: "No description available", + campaignType = null + ) + } + + fun toMap() = mutableMapOf( + "name" to name.name, + "description" to description, + ).apply { + campaignType?.let { this["campaignType"] = it.name } + } + + fun toJsonString(): String = Json.encodeToString(toMap()) +} diff --git a/src/NativeReactNativeCmp.ts b/src/NativeReactNativeCmp.ts index 3ea041a..3fd076a 100644 --- a/src/NativeReactNativeCmp.ts +++ b/src/NativeReactNativeCmp.ts @@ -210,7 +210,14 @@ export type SPBuildOptions = { } export type SPErrorName = - | "Unknown"; + | "Unknown" + | "NoInternetConnection" + | "LoadMessagesError" + | "RenderingAppError" + | "ReportActionError" + | "ReportCustomConsentError" + | "AndroidNoIntentFound" + | string; export type SPError = { name: SPErrorName; From ec2c0e9983be12e8d210bfa2e78223fd7a239e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Herculano?= Date: Wed, 10 Sep 2025 15:28:50 +0200 Subject: [PATCH 5/8] iOS: dismiss view on onError --- ios/RNSPCmp.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/RNSPCmp.swift b/ios/RNSPCmp.swift index e030129..b5972bf 100644 --- a/ios/RNSPCmp.swift +++ b/ios/RNSPCmp.swift @@ -140,6 +140,7 @@ import React public func onError(error: SPError) { print("Something went wrong", error) + rootViewController?.dismiss(animated: true) delegate?.onError(RNSPError(error)) } From 7c49dedc2ffa02c252bee6fae1a733eb8ba23dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Herculano?= Date: Wed, 10 Sep 2025 15:29:11 +0200 Subject: [PATCH 6/8] iOS: expand RNSPErrorName's --- ios/RNSPError.swift | 53 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/ios/RNSPError.swift b/ios/RNSPError.swift index d1c21dc..2b92dc5 100644 --- a/ios/RNSPError.swift +++ b/ios/RNSPError.swift @@ -8,17 +8,56 @@ import ConsentViewController public enum RNSPErrorName: String { - case unknown = "Unknown" + case unknown = "Unknown" + case noInternetConnection = "NoInternetConnection" + case loadMessagesError = "LoadMessagesError" + case renderingAppError = "RenderingAppError" + case reportActionError = "ReportActionError" + case reportCustomConsentError = "ReportCustomConsentError" + case androidNoIntentFound = "AndroidNoIntentFound" // (iOS: no equivalent) - init(_ error: SPError) { - switch error { - default: - self = .unknown + init(_ error: SPError) { + switch error { + + // MARK: - No Internet + case is NoInternetConnection: + self = .noInternetConnection + + // MARK: - Load Messages errors (/get_messages, /message/gdpr, /message/ccpa) + case is InvalidResponseGetMessagesEndpointError, + is InvalidResponseMessageGDPREndpointError, + is InvalidResponseMessageCCPAEndpointError: + self = .loadMessagesError + + // MARK: - Rendering App / WebView / JSReceiver issues + case is RenderingAppError, + is RenderingAppTimeoutError, + is UnableToInjectMessageIntoRenderingApp, + is UnableToLoadJSReceiver, + is WebViewError, + is WebViewConnectionTimeOutError, + is InvalidEventPayloadError, + is InvalidOnActionEventPayloadError, + is InvalidURLError: + self = .renderingAppError + + // MARK: - Report Action + case is ReportActionError, + is InvalidReportActionEvent: + self = .reportActionError + + // MARK: - Custom Consent reporting + case is InvalidResponseCustomError, + is InvalidResponseDeleteCustomError, + is PostingCustomConsentWithoutConsentUUID: + self = .reportCustomConsentError + + default: + self = .unknown + } } - } } - @objcMembers public class RNSPError: NSObject { public let name: RNSPErrorName public let errorDescription: String From ed66178846b82084555d2640c6ad3ccb122578f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Herculano?= Date: Wed, 10 Sep 2025 15:36:25 +0200 Subject: [PATCH 7/8] added a section on SPError to the README --- README.md | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6a78aac..cd0e7de 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Refer to the table below regarding the different campaigns that can be implement | `onSPUIFinished(callback: () => {})` | Called when the native SDKs is done removing the consent UI from the foreground. | | `onFinished(callback: () => {})` | Called when all UI and network processes are finished. User consent is stored on the local storage of each platform (`UserDefaults` for iOS and `SharedPrefs` for Android). And it is safe to retrieve consent data with `getUserData` | | `onMessageInactivityTimeout(callback: () => {})` | Called when the user becomes inactive while viewing a consent message. This allows your app to respond to user inactivity events. | -| `onError(callback: (description: string) => {})` | Called if something goes wrong. | +| `onError(callback: (error: SPError) => {})` | Called if something goes wrong. | ### Call `loadMessages` @@ -286,14 +286,30 @@ In order to use the authenticated consent all you need to do is replace `.loadMe If our APIs have a consent profile associated with that token `"JohnDoe"` the SDK will bring the consent profile from the server, overwriting whatever was stored in the device. If none is found, the session will be treated as a new user. +## The `SPError` object +```ts +export type SPErrorName = + | "Unknown" + | "NoInternetConnection" + | "LoadMessagesError" + | "RenderingAppError" + | "ReportActionError" + | "ReportCustomConsentError" + | "AndroidNoIntentFound" + | string; + +export type SPError = { + name: SPErrorName; + description: string; + campaignType?: SPCampaignType; +}; +``` +Notice `campaignType` is optional. Not all errors cases contain that information and on Android that data is not provided by the native SDK. + ## Complete App example Complete app example for iOS and Android can be found in the [`/example`](/example/) folder of the SDK. -## Known issues - -On iOS, reloading the app's bundle (ie. pressing r while the emulator is open or on Webpack's console) causes React Native to stop emitting events. This issue is being investigated and it's pottentially within React Native itself. - ## License MIT From 6f1cd9ceffb8d882f6b8f75f06abccc781802781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Herculano?= Date: Wed, 10 Sep 2025 15:38:04 +0200 Subject: [PATCH 8/8] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cd0e7de..781344e 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ export default function App() { consentManager.current?.getUserData().then(setUserData); }); consentManager.current?.onAction(({ actionType }) => { - console.log(`User took action ${actionType}`) + console.log(`User took action ${actionType}`) }); consentManager.current?.onMessageInactivityTimeout(() => { console.log("User became inactive")