import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; const LINKING_ERROR = `The package 'react-native-qualtrics' doesn't seem to be linked. Make sure: \n\n` + Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + '- You rebuilt the app after installing the package\n' + '- You are not using Expo Go\n'; // @ts-expect-error const isTurboModuleEnabled = global.__turboModuleProxy != null; const RNQualtricsDigitalModule = isTurboModuleEnabled ? require('./NativeRNQualtricsDigital').default : NativeModules.RNQualtricsDigital; export const RNQualtricsDigital = RNQualtricsDigitalModule ? RNQualtricsDigitalModule : new Proxy( {}, { get() { throw new Error(LINKING_ERROR); }, } ); const Q_LOGTAG = 'Qualtrics: '; const REACT_NATIVE_PROPERTY = 'Qualtrics_IS_REACT_NATIVE'; /* Set up validating function parameter types*/ // Parameter IDs // Add constant for new function parameters const BRAND_ID = 'brandid'; const PROJECT_ID = 'projectid'; const INTERCEPT_ID = 'interceptid'; const EXT_REF_ID = 'extrefid'; const CALLBACK = 'callback'; const VIEW_NAME = 'viewName'; const PROPERTY_KEY = 'propertyKey'; const PROPERTY_STRING = 'propertyString'; const PROPERTY_NUMBER = 'propertyNumber'; const PROPERTY_ASSET = 'propertyAsset'; const INITIALIZE_PROJECT_EVENT = 'initializeProjectEvent'; const INITIALIZATION_EVENT = 'initializeEvent'; const EVALUATE_EVENT = 'evaluateEvent'; const EVALUATE_PROJECT_EVENT = 'evaluateProjectEvent'; const EVALUATE_INTERCEPT_EVENT = 'evaluateInterceptEvent'; // Parameter Types // add constant for new function parameter types const STRING_TYPE = 'string'; const FUNCTION_TYPE = 'function'; const NUMBER_TYPE = 'number'; // Parameter IDs to Parameter Types Dictionary // add parameter ID as key and parameter type as value const EXPECTED_PARAMETER_TYPES = { [BRAND_ID]: STRING_TYPE, [PROJECT_ID]: STRING_TYPE, [INTERCEPT_ID]: STRING_TYPE, [EXT_REF_ID]: STRING_TYPE, [CALLBACK]: FUNCTION_TYPE, [VIEW_NAME]: STRING_TYPE, [PROPERTY_KEY]: STRING_TYPE, [PROPERTY_STRING]: STRING_TYPE, [PROPERTY_NUMBER]: NUMBER_TYPE, [PROPERTY_ASSET]: STRING_TYPE, }; const eventEmitter = Platform.OS === 'android' ? new NativeEventEmitter() : new NativeEventEmitter(RNQualtricsDigital); /* End validate parameter types setup */ /** * @class * @classdesc a class used to hold the results of the initialization methods * @property {boolean} passed - whether or not initialization succeeded * @property {string} surveyUrl - initialization message used to determine initialization status */ export class InitializationResult { passed: boolean; message: string; constructor(passed: boolean, message: string) { this.passed = passed; this.message = message; } } type TargetingResultStatus = | 'passed' | 'failedLogic' | 'sampledOut' | 'multipleDisplayPrevented' | 'contactFrequencyFailed' | 'error' | 'inactive' | undefined; type TargetingResultType = { passed?: boolean; targetingResultStatus?: TargetingResultStatus; error?: string; creativeType?: string; surveyUrl?: string; }; /** * @class * @classdesc a class used to hold the results of the evaluation methods * @property {boolean} passed - whether or not target evaluation succeeded * @property {string} targetingResultStatus - one of: * "passed" | "failedLogic" | "sampledOut" | "multipleDisplayPrevented" | * "contactFrequencyFailed" | "error" | "inactive" * * Android-only prop * * @property {string} error - error from the last evaluation of the intercept if * applicable; * * Android-only prop * * @property {string} surveyUrl - link to the survey in Creative where logic evaluation passed. url used to display survey in display() * @property {string} creativeType - the type of the creative associated with the passing action set. */ export class TargetingResult { passed?: boolean; targetingResultStatus: TargetingResultStatus; error?: string; creativeType?: string; surveyUrl?: string; constructor({ passed, targetingResultStatus, error, surveyUrl, creativeType, }: TargetingResultType = {}) { this.passed = passed; this.targetingResultStatus = targetingResultStatus; this.error = error; this.surveyUrl = surveyUrl; this.creativeType = creativeType; } /** * @method recordClick() - void method * @description records a click to the server associated with intercept */ recordClick(): void { try { RNQualtricsDigital.recordClick(); } catch (error) { console.log(Q_LOGTAG + 'recordClick() failed -- ' + error); } } /** * @method recordImpression() - void method * @description records an impression to the server associated with intercept */ recordImpression(): void { try { RNQualtricsDigital.recordImpression(); } catch (error) { console.log(Q_LOGTAG + 'recordImpression() failed -- ' + error); } } } /** * validateParameterTypes() - void utility method that checks types in runtime. * Suppressed with ts-ignore by design. * throws error if function parameters do not match expected parameter types * @param parameters – any type of parameter */ function validateParameterTypes(parameters: any): void { for (let p_key in parameters) { if ( parameters.hasOwnProperty(p_key) && EXPECTED_PARAMETER_TYPES.hasOwnProperty(p_key) ) { let paramType = typeof parameters[p_key]; // @ts-ignore if (paramType !== EXPECTED_PARAMETER_TYPES[p_key]) { throw new TypeError( // @ts-ignore `${p_key} parameter did not match expected type of ${EXPECTED_PARAMETER_TYPES[p_key]}` ); } } } } function isWaitingForEventToFinish(eventType: string) { return ( eventEmitter && eventEmitter.listenerCount(eventType) && eventEmitter.listenerCount(eventType) > 0 ); } /** initialize() * @description initializes Qualtrics singleton with specified params * @deprecate marked for future deprecation with version 2.0.0 * @param brandid - intercept brand id * @param projectid - intercept project/zone id * @param interceptid - intercept id * @param {callback} [callback] - optional method called, if defined, to handle the InitializationResult * @returns {InitializationResult} in callback */ function initialize( brandid: string, projectid: string, interceptid: string, callback?: (result: InitializationResult) => void ) { // Build dictionary of required parameters for type validation let functionParameters = { [BRAND_ID]: brandid, [PROJECT_ID]: projectid, [INTERCEPT_ID]: interceptid, }; try { RNQualtricsDigital.setString(REACT_NATIVE_PROPERTY, 'true'); if (typeof callback === 'undefined') { validateParameterTypes(functionParameters); RNQualtricsDigital.initialize(brandid, projectid, interceptid); } else { // Add callback to required parameters // @ts-ignore functionParameters[CALLBACK] = callback; validateParameterTypes(functionParameters); if (isWaitingForEventToFinish(INITIALIZATION_EVENT)) { callback( new InitializationResult(false, 'Initialization already in progress') ); return; } eventEmitter.addListener(INITIALIZATION_EVENT, (result) => { if (result !== null) { callback(new InitializationResult(result.passed, result.message)); } else { callback( new InitializationResult( false, 'Invalid InitializationResult Received' ) ); } eventEmitter.removeAllListeners(INITIALIZATION_EVENT); }); RNQualtricsDigital.initializeWithCompletion( brandid, projectid, interceptid ); } } catch (error) { console.log(Q_LOGTAG + 'initialize() failed -- ' + error); } } /** initializeProject() - void method * @description initializes Qualtrics singleton with specified params * @param brandId - intercept brand id * @param projectId - intercept project/zone id * @param {callback} [callback] - optional method called, if defined, to handle initializationResults * @return {InitializationResult}s to the callback */ function initializeProject( brandId: string, projectId: string, callback: (initializationResults: { [interceptId: string]: InitializationResult; }) => void ): void { initializeProjectWithExtRefId(brandId, projectId, '', callback); } /** initializeProjectWithExtRefId() - void method * @description initializes Qualtrics singleton with specified params * @param brandid - intercept brand id * @param projectid - intercept project/zone id * @param extrefid - external reference id * @param {callback} [callback] - optional method called, if defined, to handle initializationResults * @return {InitializationResult}s to the callback */ function initializeProjectWithExtRefId( brandid: string, projectid: string, extrefid: string, callback: (initializationResults: { [interceptId: string]: InitializationResult; }) => void ): void { let functionParameters = { [BRAND_ID]: brandid, [PROJECT_ID]: projectid, [EXT_REF_ID]: extrefid, [CALLBACK]: callback, }; try { RNQualtricsDigital.setString(REACT_NATIVE_PROPERTY, 'true'); validateParameterTypes(functionParameters); if (isWaitingForEventToFinish(INITIALIZE_PROJECT_EVENT)) { let errorResult: { [interceptId: string]: InitializationResult } = {}; errorResult.ERROR = new InitializationResult( false, 'Invalid InitializationResult Received' ); callback(errorResult); return; } eventEmitter.addListener(INITIALIZE_PROJECT_EVENT, (result) => { if (result !== null) { var initializationResults = {}; for (var interceptId in result) { if (Object.prototype.hasOwnProperty.call(result, interceptId)) { if (result[interceptId].message && result[interceptId].passed) { // @ts-ignore initializationResults[interceptId] = new InitializationResult( result[interceptId].passed, result[interceptId].message ); } } } callback(initializationResults); } else { let errorResult: { [interceptId: string]: InitializationResult } = {}; errorResult.ERROR = new InitializationResult( false, 'Invalid InitializationResult Received' ); callback(errorResult); } eventEmitter.removeAllListeners(INITIALIZE_PROJECT_EVENT); }); RNQualtricsDigital.initializeProject(brandid, projectid, extrefid); } catch (error) { console.log(Q_LOGTAG + 'initializeProject() failed -- ' + error); } } /** evaluateTargetingLogic() - void method * @deprecate marked for future deprecation with version 2.0.0 * @description evaluates current intercept and calls callback * @return {TargetingResult} in callback * @param callback - method called, to handle TargetingResult, must be defined */ function evaluateTargetingLogic( callback: (result: TargetingResult) => void ): void { // Build dictionary of required parameters for type validation const functionParameters = { [CALLBACK]: callback, }; try { validateParameterTypes(functionParameters); if (isWaitingForEventToFinish(EVALUATE_EVENT)) { callback( new TargetingResult({ passed: false, targetingResultStatus: 'error', error: 'Evaluation already in progress', surveyUrl: 'Evaluation already in progress', }) ); return; } eventEmitter.addListener(EVALUATE_EVENT, (result) => { if (result !== null) { callback( new TargetingResult({ passed: result.passed, targetingResultStatus: result.targetingResultStatus, error: result.error, surveyUrl: result.surveyUrl, }) ); } else { callback( new TargetingResult({ passed: false, targetingResultStatus: 'error', error: 'Invalid TargetingResult Received', surveyUrl: 'Invalid TargetingResult Received', }) ); } eventEmitter.removeAllListeners(EVALUATE_EVENT); }); RNQualtricsDigital.evaluateTargetingLogic(); } catch (error) { console.log(Q_LOGTAG + 'evaluateTargetingLogic() failed -- ' + error); } } /** evaluateProject() - void method * @description evaluates all the initialized intercepts and calls callback * @returns {TargetingResult}s in callback * @param callback - method called to handle TargetingResults, must be defined */ function evaluateProject( callback: (targetingResults: { [key: string]: TargetingResult }) => void ): void { // Build dictionary of required parameters for type validation const functionParameters = { [CALLBACK]: callback, }; try { validateParameterTypes(functionParameters); if (isWaitingForEventToFinish(EVALUATE_PROJECT_EVENT)) { let errorResult: { [key: string]: TargetingResult } = {}; errorResult.ERROR = new TargetingResult({ passed: false, targetingResultStatus: 'error', error: 'Evaluation already in progress', surveyUrl: 'Evaluation already in progress', creativeType: '', }); callback(errorResult); return; } eventEmitter.addListener(EVALUATE_PROJECT_EVENT, (result) => { if (result !== null) { let targetingResults: { [key: string]: TargetingResult } = {}; for (let interceptId in result) { if (Object.prototype.hasOwnProperty.call(result, interceptId)) { const singleResult = result[interceptId]; targetingResults[interceptId] = new TargetingResult({ passed: singleResult.passed, targetingResultStatus: singleResult.targetingResultStatus, error: singleResult.error, surveyUrl: singleResult.surveyUrl, creativeType: singleResult.creativeType, }); } } callback(targetingResults); } else { let errorResult: { [key: string]: TargetingResult } = {}; errorResult.ERROR = new TargetingResult({ passed: false, targetingResultStatus: 'error', error: 'Invalid TargetingResult Received', surveyUrl: 'Invalid TargetingResult Received', creativeType: '', }); callback(errorResult); } eventEmitter.removeAllListeners(EVALUATE_PROJECT_EVENT); }); RNQualtricsDigital.evaluateProject(); } catch (error) { console.log(Q_LOGTAG + 'evaluateProject() failed -- ' + error); } } /** evaluateIntercept() - void method * @description evaluates the passed intercept and calls callback * @param interceptId * @param callback - method called to handle TargetingResult, must be defined * @return {TargetingResult}s in callback */ function evaluateIntercept( interceptId: string, callback: (result: TargetingResult) => void ): void { // Build dictionary of required parameters for type validation const functionParameters = { [CALLBACK]: callback, }; try { validateParameterTypes(functionParameters); if (isWaitingForEventToFinish(EVALUATE_INTERCEPT_EVENT)) { callback( new TargetingResult({ passed: false, targetingResultStatus: 'error', error: 'Evaluation already in progress', surveyUrl: 'Evaluation already in progress', creativeType: '', }) ); return; } if (!interceptId) { callback( new TargetingResult({ passed: false, targetingResultStatus: 'error', error: 'Intercept ID cannot be empty', surveyUrl: 'Intercept ID cannot be empty', creativeType: '', }) ); return; } eventEmitter.addListener(EVALUATE_INTERCEPT_EVENT, (result) => { if (result !== null) { callback( new TargetingResult({ passed: result.passed, targetingResultStatus: result.targetingResultStatus, error: result.error, surveyUrl: result.surveyUrl, creativeType: result.creativeType, }) ); } else { callback( new TargetingResult({ passed: false, targetingResultStatus: 'error', error: 'Invalid TargetingResult Received', surveyUrl: 'Invalid TargetingResult Received', creativeType: '', }) ); } eventEmitter.removeAllListeners(EVALUATE_INTERCEPT_EVENT); }); RNQualtricsDigital.evaluateIntercept(interceptId); } catch (error) { console.log(Q_LOGTAG + 'evaluateIntercept() failed -- ' + error); } } /** display() - bool return method * @description displays creative in the view controller * @param {boolean} autoCloseSurvey Parameter that enables auto close for displayed survey * @returns {Promise} Promise object representing a bool */ function display(autoCloseSurvey: boolean = false): Promise { const closeSurvey = autoCloseSurvey ?? false; return new Promise((resolve, reject) => { try { RNQualtricsDigital.display(closeSurvey, (result: boolean) => { if (result) { resolve(true); } reject(false); }); } catch (error) { console.log(Q_LOGTAG + 'display() failed -- ' + error); reject(false); } }); } /** displayTarget() - void method * @description displays evaluated TargetingResult in webview * @param {string} surveyUrl - Optional link to the surveyUrl * @param {string} autoCloseSurvey - Optional boolean for auto closing survey at the end */ function displayTarget(surveyUrl: string | null = null, autoCloseSurvey: boolean = false): void { const closeSurvey = autoCloseSurvey ?? false; if (surveyUrl === '') { surveyUrl = null; } try { RNQualtricsDigital.displayTarget(surveyUrl, closeSurvey, (result: boolean) => { // Display target failed; log error if (!result) { console.log( Q_LOGTAG + 'displayTarget() failed -- no TargetingResult with valid target URL' ); } }); } catch (error) { console.log(Q_LOGTAG + 'displayTarget() failed -- ' + error); } } /** displayIntercept() - bool return method * @description displays creative in the view controller * @param {string} interceptId - Id of evaluated intercept that should be displayed * @param {string} autoCloseSurvey - Optional boolean for auto closing survey at the end * @returns {Promise} Promise object representing a bool */ function displayIntercept(interceptId: string, autoCloseSurvey: boolean = false): Promise { const closeSurvey = autoCloseSurvey ?? false; return new Promise((resolve, reject) => { try { RNQualtricsDigital.displayIntercept(interceptId, closeSurvey, (result: boolean) => { if (result) { resolve(true); } reject(false); }); } catch (error) { console.log(Q_LOGTAG + 'displayIntercept() failed -- ' + error); reject(false); } }); } /** setString() - void method * @description sets string value for specified key in properties * @param propertyKey - Key for value * @param propertyString - String value to assign with key */ function setString(propertyKey: string, propertyString: string): void { // Build dictionary of required parameters for type validation const functionParameters = { [PROPERTY_KEY]: propertyKey, [PROPERTY_STRING]: propertyString, }; try { validateParameterTypes(functionParameters); RNQualtricsDigital.setString(propertyKey, propertyString); } catch (error) { console.log(Q_LOGTAG + 'setString() failed -- ' + error); } } /** setNumber() - void method * @description sets double value for specified key in properties * @param propertyKey - Key for value * @param propertyNumber - Numeric value to assign with key */ function setNumber(propertyKey: string, propertyNumber: number): void { // Build dictionary of required parameters for type validation const functionParameters = { [PROPERTY_KEY]: propertyKey, [PROPERTY_NUMBER]: propertyNumber, }; try { validateParameterTypes(functionParameters); RNQualtricsDigital.setNumber(propertyKey, propertyNumber); } catch (error) { console.log(Q_LOGTAG + 'setNumber() failed -- ' + error); } } /** setDateTime() - void method * @description sets current datetime value for specified key in properties * @param propertyKey - Key for value */ function setDateTime(propertyKey: string): void { // Build dictionary of required parameters for type validation const functionParameters = { [PROPERTY_KEY]: propertyKey, }; try { validateParameterTypes(functionParameters); RNQualtricsDigital.setDateTime(propertyKey); } catch (error) { console.log(Q_LOGTAG + 'setDateTime() failed -- ' + error); } } /** setLastDisplayTimeForIntercept() - void method * @description sets the lastDisplayedTime for specified interceptId to now * @param {string} interceptId - The ID of the intercept for which the last display time is being set */ function setLastDisplayTimeForIntercept(interceptId: string): void { try { RNQualtricsDigital.setLastDisplayTimeForIntercept(interceptId); } catch (error) { console.log( Q_LOGTAG + 'setLastDisplayTimeForIntercept() failed --' + error ); } } /** setNotificationIconAsset() - void method * @description Sets string file name for notificationIconAsset in properties * @param {string} propertyAsset - File name of Image stored in Drawable folder of Android Project */ function setNotificationIconAsset(propertyAsset: string): void { // Build dictionary of required parameters for type validation const functionParameters = { [PROPERTY_ASSET]: propertyAsset, }; try { validateParameterTypes(functionParameters); RNQualtricsDigital.setNotificationIconAsset(propertyAsset); } catch (error) { console.log(Q_LOGTAG + 'setNotificationIconAsset() failed -- ' + error); } } /** registerViewVisit() - void method * @description record visits to a particular view * @param viewName - name of view to record */ function registerViewVisit(viewName: string): void { // Build dictionary of required parameters for type validation const functionParameters = { [VIEW_NAME]: viewName, }; try { validateParameterTypes(functionParameters); RNQualtricsDigital.registerViewVisit(viewName); } catch (error) { console.log(Q_LOGTAG + 'registerViewVisit() failed -- ' + error); } } /** resetTimer() - void method * @description resets timer used for Time Spent In App */ function resetTimer(): void { try { RNQualtricsDigital.resetTimer(); } catch (error) { console.log(Q_LOGTAG + 'resetTimer() failed -- ' + error); } } /** resetViewCounter() - void method * @description resets view count used for View Count */ function resetViewCounter(): void { try { RNQualtricsDigital.resetViewCounter(); } catch (error) { console.log(Q_LOGTAG + 'resetViewCounter() failed -- ' + error); } } /** * * @returns Returns an array of all the initialized intercepts */ function getInitializedIntercepts(): Promise> { return new Promise>((resolve) => { try { RNQualtricsDigital.getInitializedIntercepts( (intercepts: Array) => { resolve(intercepts); } ); } catch (error) { resolve(['']); } }); } /** * * @returns Returns an array of all the passing intercepts */ function getPassingIntercepts(): Promise> { return new Promise>((resolve) => { try { RNQualtricsDigital.getPassingIntercepts((intercepts: Array) => { resolve(intercepts); }); } catch (error) { resolve(['']); } }); } export default { initialize: initialize, initializeProject: initializeProject, initializeProjectWithExtRefId: initializeProjectWithExtRefId, evaluateTargetingLogic: evaluateTargetingLogic, evaluateIntercept: evaluateIntercept, evaluateProject: evaluateProject, display: display, displayTarget: displayTarget, displayIntercept: displayIntercept, setString: setString, setNumber: setNumber, setDateTime: setDateTime, setLastDisplayTimeForIntercept: setLastDisplayTimeForIntercept, setNotificationIconAsset: setNotificationIconAsset, registerViewVisit: registerViewVisit, resetTimer: resetTimer, resetViewCounter: resetViewCounter, getInitializedIntercepts: getInitializedIntercepts, getPassingIntercepts: getPassingIntercepts, };