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
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
56 changes: 56 additions & 0 deletions android/src/main/java/com/sourcepoint/reactnativecmp/RNSPError.kt
Original file line number Diff line number Diff line change
@@ -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())
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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) {}
Expand All @@ -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()
}
Expand Down
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1996,4 +1996,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: decdc7519d77aa5eae65b167fa59bcfce25e15d2

COCOAPODS: 1.15.2
COCOAPODS: 1.16.2
27 changes: 27 additions & 0 deletions ios/RNSPAction.swift
Original file line number Diff line number Diff line change
@@ -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 }
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// RNSourcepointActionType.swift
// RNSPActionType.swift
// sourcepoint-react-native-cmp
//
// Created by Andre Herculano on 31/5/24.
Expand All @@ -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 {
Expand Down
43 changes: 43 additions & 0 deletions ios/RNSPCampaignType.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
34 changes: 6 additions & 28 deletions ios/RNSourcepointCmp.swift → ios/RNSPCmp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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? {
Expand Down Expand Up @@ -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) {
Expand All @@ -163,7 +140,8 @@ import React

public func onError(error: SPError) {
print("Something went wrong", error)
delegate?.onError(description: error.description)
rootViewController?.dismiss(animated: true)
delegate?.onError(RNSPError(error))
}

public func dismissMessage() {
Expand Down
91 changes: 91 additions & 0 deletions ios/RNSPError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// RNError.swift
// Pods
//
// Created by Andre Herculano on 5/9/25.
//

import ConsentViewController

public enum RNSPErrorName: String {
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 {

// 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
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 }
}
}
Loading
Loading