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
88 changes: 54 additions & 34 deletions android/src/main/java/io/didomi/reactnative/DidomiModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.didomi.sdk.Didomi
import io.didomi.sdk.DidomiInitializeParameters
import io.didomi.sdk.DidomiMultiUserParameters
import io.didomi.sdk.DidomiUserParameters
import io.didomi.sdk.DidomiWidgetParameters
import io.didomi.sdk.events.*
import io.didomi.sdk.exceptions.DidomiNotReadyException
import io.didomi.sdk.models.CurrentUserStatus
Expand Down Expand Up @@ -425,14 +426,8 @@ class DidomiModule(reactContext: ReactApplicationContext) : DidomiModuleSpec(rea

@ReactMethod
override fun setupUI(promise: Promise) {
try {
runOnUiThread {
Didomi.getInstance().setupUI(reactContext.currentActivity as? FragmentActivity)
}
promise.resolve(0)
} catch (e: Exception) {
Log.e("setupUI", "An error occurred while setting up the UI", e)
promise.reject(e)
resolveOnUiThread(promise, "setupUI", "An error occurred while setting up the UI") {
Didomi.getInstance().setupUI(reactContext.currentActivity as? FragmentActivity)
}
}

Expand Down Expand Up @@ -698,6 +693,15 @@ class DidomiModule(reactContext: ReactApplicationContext) : DidomiModuleSpec(rea
}
}

@ReactMethod
override fun isWidgetVisible(widgetId: String?, promise: Promise) {
try {
promise.resolve(Didomi.getInstance().isWidgetVisible(widgetId?.let { DidomiWidgetParameters(widgetId = it) }))
} catch (e: DidomiNotReadyException) {
promise.reject(e)
}
}

@ReactMethod
override fun isPreferencesVisible(promise: Promise) {
try {
Expand Down Expand Up @@ -995,42 +999,41 @@ class DidomiModule(reactContext: ReactApplicationContext) : DidomiModuleSpec(rea

@ReactMethod
override fun showNotice(promise: Promise) {
try {
runOnUiThread {
Didomi.getInstance().showNotice(reactContext.currentActivity as? FragmentActivity)
}
promise.resolve(0)
} catch (e: Exception) {
Log.e("showNotice", "An error occurred while showing the notice", e)
promise.reject(e)
resolveOnUiThread(promise, "showNotice", "An error occurred while showing the notice") {
Didomi.getInstance().showNotice(reactContext.currentActivity as? FragmentActivity)
}
}

@ReactMethod
override fun forceShowNotice(promise: Promise) {
try {
runOnUiThread {
Didomi.getInstance().forceShowNotice(reactContext.currentActivity as? FragmentActivity)
}
promise.resolve(0)
} catch (e: Exception) {
Log.e("forceShowNotice", "An error occurred while force-showing the notice", e)
promise.reject(e)
resolveOnUiThread(promise, "forceShowNotice", "An error occurred while force-showing the notice") {
Didomi.getInstance().forceShowNotice(reactContext.currentActivity as? FragmentActivity)
}
}

@ReactMethod
override fun showPreferences(view: String?, promise: Promise) {
try {
runOnUiThread {
view?.also {
Didomi.getInstance().showPreferences(reactContext.currentActivity as? FragmentActivity, view)
} ?: Didomi.getInstance().showPreferences(reactContext.currentActivity as? FragmentActivity)
}
promise.resolve(0)
} catch (e: Exception) {
Log.e("showPreferences", "An error occurred while showing the notice", e)
promise.reject(e)
resolveOnUiThread(promise, "showPreferences", "An error occurred while showing the notice") {
view?.also {
Didomi.getInstance().showPreferences(reactContext.currentActivity as? FragmentActivity, view)
} ?: Didomi.getInstance().showPreferences(reactContext.currentActivity as? FragmentActivity)
}
}

@ReactMethod
override fun showWidget(widgetId: String?, layerName: String?, promise: Promise) {
resolveOnUiThread(promise, "showWidget", "An error occurred while showing the widget") {
Didomi.getInstance().showWidget(
reactContext.currentActivity as? FragmentActivity,
DidomiWidgetParameters(widgetId = widgetId, layerName = layerName)
)
}
}

@ReactMethod
override fun hideWidget(promise: Promise) {
resolveOnUiThread(promise, "hideWidget", "An error occurred while hiding the widget") {
Didomi.getInstance().hideWidget()
}
}

Expand Down Expand Up @@ -1278,6 +1281,23 @@ class DidomiModule(reactContext: ReactApplicationContext) : DidomiModuleSpec(rea
prepareEvent(eventName, params)
}

/**
* Run [block] on the UI thread, then resolve the [promise] (or reject it if [block] throws).
* The block runs later on the UI thread, so its exceptions (e.g. DidomiNotReadyException) must be
* caught there: an uncaught exception on the UI thread would crash the app.
*/
private fun resolveOnUiThread(promise: Promise, tag: String, errorMessage: String, block: () -> Unit) {
runOnUiThread {
try {
block()
promise.resolve(0)
} catch (e: Exception) {
Log.e(tag, errorMessage, e)
promise.reject(e)
}
}
}

// Required to transform from array to variadic.
private fun readableArrayToStringArray(readableArray: ReadableArray) = readableArray.toSet().toTypedArray()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ abstract class DidomiModuleSpec(reactContext: ReactApplicationContext) :
open fun isUserLegitimateInterestStatusPartial(promise: Promise): Unit = stub()
open fun isNoticeVisible(promise: Promise): Unit = stub()
open fun isPreferencesVisible(promise: Promise): Unit = stub()
open fun showWidget(widgetId: String?, layerName: String?, promise: Promise): Unit = stub()
open fun hideWidget(promise: Promise): Unit = stub()
open fun isWidgetVisible(widgetId: String?, promise: Promise): Unit = stub()
open fun isError(promise: Promise): Unit = stub()
open fun isReady(promise: Promise): Unit = stub()
open fun clearUser(promise: Promise): Unit = stub()
Expand Down
12 changes: 12 additions & 0 deletions ios/Didomi.m
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,18 @@ @interface RCT_EXTERN_MODULE(Didomi, RCTEventEmitter)
RCT_EXTERN_METHOD(isPreferencesVisible:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(showWidget:(nullable NSString *)widgetId
layerName:(nullable NSString *)layerName
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

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

RCT_EXTERN_METHOD(isWidgetVisible:(nullable NSString *)widgetId
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(getTranslatedText:(NSString *)key
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
Expand Down
23 changes: 23 additions & 0 deletions ios/RNDidomi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,29 @@ class RNDidomi: RCTEventEmitter {
resolve(0)
}

@objc(showWidget:layerName:resolve:reject:)
dynamic func showWidget(widgetId: String?, layerName: String?, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) {
DispatchQueue.main.async {
let parameters = DidomiWidgetParameters(widgetID: widgetId, layerName: layerName)
Didomi.shared.showWidget(controller: RCTPresentedViewController(), parameters: parameters)
}

resolve(0)
}

@objc(hideWidget:reject:)
dynamic func hideWidget(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) {
DispatchQueue.main.async {
Didomi.shared.hideWidget()
}
resolve(0)
}

@objc(isWidgetVisible:resolve:reject:)
dynamic func isWidgetVisible(widgetId: String?, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) {
resolve(Didomi.shared.isWidgetVisible(parameters: DidomiWidgetParameters(widgetID: widgetId, layerName: nil)))
}

@objc(hidePreferences:reject:)
dynamic func hidePreferences(resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) {
DispatchQueue.main.async {
Expand Down
10 changes: 10 additions & 0 deletions sample/src/Getters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ export default function Getters() {
}}
/>

<Getter
name="isWidgetVisible"
call={async () => {
return await Didomi.isWidgetVisible();
}}
test={() => {
return true;
}}
/>

<Getter
name="getUserStatus"
call={async () => {
Expand Down
20 changes: 20 additions & 0 deletions sample/src/Methods.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,26 @@ export default function Methods() {
}}
/>

<MethodCall
name="showWidget"
call={() => {
Didomi.showWidget({ widgetId: 'widget_cpra' });
}}
test={() => {
return true;
}}
/>

<MethodCall
name="hideWidget"
call={() => {
Didomi.hideWidget();
}}
test={() => {
return true;
}}
/>

<MethodCall
name="reset"
call={() => {
Expand Down
25 changes: 24 additions & 1 deletion src/Didomi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import NativeDidomi from './specs/NativeDidomi';
import { DidomiListener } from './DidomiListener';
import { DidomiEventType, Purpose, Vendor, UserStatus, CurrentUserStatus, VendorStatus, UserAuthParams, DidomiInitializeParameters, DidomiUserParameters, UserAuthWithEncryptionParams, UserAuthWithHashParams, DidomiMultiUserParameters } from './DidomiTypes';
import { DidomiEventType, Purpose, Vendor, UserStatus, CurrentUserStatus, VendorStatus, UserAuthParams, DidomiInitializeParameters, DidomiUserParameters, UserAuthWithEncryptionParams, UserAuthWithHashParams, DidomiMultiUserParameters, DidomiWidgetParameters } from './DidomiTypes';
import { DIDOMI_USER_AGENT_NAME, DIDOMI_VERSION } from './Constants';
import { CurrentUserStatusTransaction, createCurrentUserStatusTransaction } from './CurrentUserStatusTransaction';

Expand Down Expand Up @@ -468,6 +468,14 @@ export const Didomi = {
*/
isPreferencesVisible: (): Promise<boolean> => RNDidomi.isPreferencesVisible(),

/**
* Check if a widget is currently displayed.
* **Beta**: this API is in beta - Privacy widget feature is still in development
* @param widgetId: optional identifier of the widget to check. If not provided, any widget is considered.
* @returns: **true** if the widget (or any widget if no ID is provided) is displayed, **false** otherwise.
*/
isWidgetVisible: (widgetId?: string): Promise<boolean> => RNDidomi.isWidgetVisible(widgetId ?? null),

isError: (): Promise<boolean> => RNDidomi.isError(),

/**
Expand Down Expand Up @@ -691,6 +699,21 @@ export const Didomi = {
*/
showPreferences: (view?: string): Promise<void> => RNDidomi.showPreferences(view),

/**
* Show a widget when/if the SDK is ready.
* **Beta**: this API is in beta - Privacy widget feature is still in development
* Widgets are only available when the notice is rendered by the Web SDK.
* @param parameters: optional widget parameters. If `widgetId` is not provided, the widget is selected by the Rules Engine.
* If `layerName` is not provided, the widget is displayed at its default layer.
*/
showWidget: (parameters?: DidomiWidgetParameters): Promise<void> => RNDidomi.showWidget(parameters?.widgetId ?? null, parameters?.layerName ?? null),

/**
* Hide the currently displayed widget, if any. Unsaved changes are discarded.
* **Beta**: this API is in beta - Privacy widget feature is still in development
*/
hideWidget: (): Promise<void> => RNDidomi.hideWidget(),

/**
* Remove all consents for the user
*/
Expand Down
26 changes: 23 additions & 3 deletions src/DidomiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,11 @@ export interface IntegrationErrorEvent {
}

/**
* A widget was displayed.
* Emitted with the {@link DidomiEventType#SHOW_WIDGET} event type.
* A widget layer was displayed.
* Emitted with the {@link DidomiEventType#SHOW_WIDGET} event type when the widget is displayed, and again
* each time the user navigates to another layer of the same widget (e.g. from `notice` to `purposes` and back),
* with no {@link DidomiEventType#HIDE_WIDGET} in between. The widget is closed only when
* {@link DidomiEventType#HIDE_WIDGET} is emitted.
* @interface
*/
export interface ShowWidgetEvent {
Expand All @@ -205,7 +208,24 @@ export interface ShowWidgetEvent {
*/
widgetId?: string;
/**
* Name of the layer at which the widget was displayed.
* Name of the layer that was displayed.
* @property
*/
layerName?: string;
}

/**
* Parameters used to display a widget.
* @interface
*/
export interface DidomiWidgetParameters {
/**
* Identifier of the widget to display. If not provided, the widget is selected by the Rules Engine.
* @property
*/
widgetId?: string;
/**
* Name of the layer at which the widget should be displayed (e.g. `notice`, `purposes` or `vendors`). If not provided, the default layer is used.
* @property
*/
layerName?: string;
Expand Down
51 changes: 51 additions & 0 deletions src/__tests__/Didomi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ jest.mock('../specs/NativeDidomi', () => ({
default: {
getUserCountryCode: jest.fn(),
getUserRegionCode: jest.fn(),
showWidget: jest.fn(),
hideWidget: jest.fn(),
isWidgetVisible: jest.fn(),
},
}));

Expand Down Expand Up @@ -34,3 +37,51 @@ describe('getUserRegionCode', () => {
await expect(Didomi.getUserRegionCode()).resolves.toBeNull();
});
});

describe('showWidget', () => {
beforeEach(() => {
RNDidomi.showWidget.mockReset();
RNDidomi.showWidget.mockResolvedValue(0);
});

it('passes null parameters when none are provided', async () => {
await Didomi.showWidget();
expect(RNDidomi.showWidget).toHaveBeenCalledWith(null, null);
});

it('passes the widget ID only', async () => {
await Didomi.showWidget({ widgetId: 'widget_cpra' });
expect(RNDidomi.showWidget).toHaveBeenCalledWith('widget_cpra', null);
});

it('passes the widget ID and layer name', async () => {
await Didomi.showWidget({ widgetId: 'widget_cpra', layerName: 'purposes' });
expect(RNDidomi.showWidget).toHaveBeenCalledWith('widget_cpra', 'purposes');
});
});

describe('hideWidget', () => {
it('calls the native module', async () => {
RNDidomi.hideWidget.mockResolvedValue(0);
await Didomi.hideWidget();
expect(RNDidomi.hideWidget).toHaveBeenCalledTimes(1);
});
});

describe('isWidgetVisible', () => {
beforeEach(() => {
RNDidomi.isWidgetVisible.mockReset();
});

it('checks any widget when no ID is provided', async () => {
RNDidomi.isWidgetVisible.mockResolvedValue(true);
await expect(Didomi.isWidgetVisible()).resolves.toBe(true);
expect(RNDidomi.isWidgetVisible).toHaveBeenCalledWith(null);
});

it('checks a specific widget', async () => {
RNDidomi.isWidgetVisible.mockResolvedValue(false);
await expect(Didomi.isWidgetVisible('widget_cpra')).resolves.toBe(false);
expect(RNDidomi.isWidgetVisible).toHaveBeenCalledWith('widget_cpra');
});
});
3 changes: 2 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export {
UserAuthWithEncryptionParams,
UserAuthWithHashParams,
DidomiInitializeParameters,
ShowWidgetEvent
ShowWidgetEvent,
DidomiWidgetParameters
} from './DidomiTypes';
Loading
Loading