diff --git a/example/src/components/Login/Login.tsx b/example/src/components/Login/Login.tsx index e32563398..9712792ed 100644 --- a/example/src/components/Login/Login.tsx +++ b/example/src/components/Login/Login.tsx @@ -44,7 +44,7 @@ export const Login = ({ navigation }: RootStackScreenProps) => { void; /** The user ID for the user */ - userId?: string; + userId?: string | null; } const IterableAppContext = createContext({ @@ -79,7 +79,7 @@ const IterableAppContext = createContext({ setLoginInProgress: () => undefined, setReturnToInboxTrigger: () => undefined, setUserId: () => undefined, - userId: undefined, + userId: null, }); const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -96,7 +96,7 @@ export const IterableAppProvider: FunctionComponent< const [apiKey, setApiKey] = useState( process.env.ITBL_API_KEY ); - const [userId, setUserId] = useState(process.env.ITBL_ID); + const [userId, setUserId] = useState(process.env.ITBL_ID ?? null); const [loginInProgress, setLoginInProgress] = useState(false); const getUserId = useCallback(() => userId ?? process.env.ITBL_ID, [userId]); @@ -196,8 +196,8 @@ export const IterableAppProvider: FunctionComponent< ); const logout = useCallback(() => { - Iterable.setEmail(undefined); - Iterable.setUserId(undefined); + Iterable.setEmail(null); + Iterable.setUserId(null); setIsLoggedIn(false); }, []); diff --git a/package.json b/package.json index 4a8479336..fb9752664 100644 --- a/package.json +++ b/package.json @@ -177,9 +177,9 @@ ] }, "codegenConfig": { - "name": "RNIterableSpec", + "name": "RNIterableAPISpec", "type": "modules", - "jsSrcsDir": "src", + "jsSrcsDir": "src/api/", "android": { "javaPackageName": "com.iterable.reactnative" } diff --git a/src/api/NativeRNIterableAPI.ts b/src/api/NativeRNIterableAPI.ts new file mode 100644 index 000000000..0ffc66799 --- /dev/null +++ b/src/api/NativeRNIterableAPI.ts @@ -0,0 +1,140 @@ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + // Initialization + initializeWithApiKey( + apiKey: string, + config: { [key: string]: string | number | boolean | undefined | string[] }, + version: string + ): Promise; + + initialize2WithApiKey( + apiKey: string, + config: { [key: string]: string | number | boolean | undefined | string[] }, + apiEndPointOverride: string, + version: string + ): Promise; + + // User management + setEmail(email: string | null, authToken?: string | null): void; + getEmail(): Promise; + setUserId(userId?: string | null, authToken?: string | null): void; + getUserId(): Promise; + + // In-app messaging + setInAppShowResponse(number: number): void; + getInAppMessages(): Promise<{ [key: string]: string | number | boolean }[]>; + getInboxMessages(): Promise<{ [key: string]: string | number | boolean }[]>; + getUnreadInboxMessagesCount(): Promise; + showMessage(messageId: string, consume: boolean): Promise; + removeMessage(messageId: string, location: number, source: number): void; + setReadForMessage(messageId: string, read: boolean): void; + setAutoDisplayPaused(autoDisplayPaused: boolean): void; + + // Tracking + trackEvent( + name: string, + dataFields?: { [key: string]: string | number | boolean } + ): void; + trackPushOpenWithCampaignId( + campaignId: number, + templateId: number | null, + messageId: string, + appAlreadyRunning: boolean, + dataFields?: { [key: string]: string | number | boolean } + ): void; + trackInAppOpen(messageId: string, location: number): void; + trackInAppClick( + messageId: string, + location: number, + clickedUrl: string + ): void; + trackInAppClose( + messageId: string, + location: number, + source: number, + clickedUrl?: string + ): void; + inAppConsume(messageId: string, location: number, source: number): void; + + // Commerce + updateCart(items: { [key: string]: string | number | boolean }[]): void; + trackPurchase( + total: number, + items: { [key: string]: string | number | boolean }[], + dataFields?: { [key: string]: string | number | boolean } + ): void; + + // User data + updateUser( + dataFields: { [key: string]: string | number | boolean }, + mergeNestedObjects: boolean + ): void; + updateEmail(email: string, authToken?: string): void; + + // Attribution + getAttributionInfo(): Promise<{ + [key: string]: string | number | boolean; + } | null>; + setAttributionInfo( + dict: { [key: string]: string | number | boolean } | null + ): void; + + // Device management + disableDeviceForCurrentUser(): void; + getLastPushPayload(): Promise<{ + [key: string]: string | number | boolean; + } | null>; + + // Content + getHtmlInAppContentForMessage( + messageId: string + ): Promise<{ [key: string]: string | number | boolean }>; + + // App links + handleAppLink(appLink: string): Promise; + + // Subscriptions + updateSubscriptions( + emailListIds: number[] | null, + unsubscribedChannelIds: number[] | null, + unsubscribedMessageTypeIds: number[] | null, + subscribedMessageTypeIds: number[] | null, + campaignId: number, + templateId: number + ): void; + + // Session tracking + startSession( + visibleRows: { [key: string]: string | number | boolean }[] + ): void; + endSession(): void; + updateVisibleRows( + visibleRows: { [key: string]: string | number | boolean }[] + ): void; + + // Auth + passAlongAuthToken(authToken?: string | null): void; + + // Wake app -- android only + wakeApp(): void; + + + // REQUIRED for RCTEventEmitter + addListener(eventName: string): void; + removeListeners(count: number): void; +} + +// Check if we're in a test environment +const isTestEnvironment = () => { + return ( + typeof jest !== 'undefined' || + process.env.NODE_ENV === 'test' || + process.env.JEST_WORKER_ID !== undefined + ); +}; + +export default isTestEnvironment() + ? undefined + : TurboModuleRegistry.getEnforcing('RNIterableAPI'); diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 000000000..9c327891b --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,6 @@ +import { NativeModules } from 'react-native'; +import BridgelessModule from './NativeRNIterableAPI'; + +export const RNIterableAPI = BridgelessModule ?? NativeModules.RNIterableAPI; + +export default RNIterableAPI; diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 5b8b07fff..9193aec4c 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -1,12 +1,12 @@ import { Linking, NativeEventEmitter, - NativeModules, Platform, } from 'react-native'; import { buildInfo } from '../../itblBuildInfo'; +import { RNIterableAPI } from '../../api'; // TODO: Organize these so that there are no circular dependencies // See https://github.com/expo/expo/issues/35100 import { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; @@ -23,7 +23,6 @@ import type { IterableCommerceItem } from './IterableCommerceItem'; import { IterableConfig } from './IterableConfig'; import { IterableLogger } from './IterableLogger'; -const RNIterableAPI = NativeModules.RNIterableAPI; const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); /* eslint-disable tsdoc/syntax */ @@ -181,7 +180,7 @@ export class Iterable { * Iterable.setEmail('my.user.name@gmail.com'); * ``` */ - static setEmail(email?: string | null, authToken?: string | null) { + static setEmail(email: string | null, authToken?: string | null) { Iterable?.logger?.log('setEmail: ' + email); RNIterableAPI.setEmail(email, authToken); @@ -197,7 +196,7 @@ export class Iterable { * }); * ``` */ - static getEmail(): Promise { + static getEmail(): Promise { Iterable?.logger?.log('getEmail'); return RNIterableAPI.getEmail(); @@ -262,7 +261,7 @@ export class Iterable { * }); * ``` */ - static getUserId(): Promise { + static getUserId(): Promise { Iterable?.logger?.log('getUserId'); return RNIterableAPI.getUserId(); @@ -325,12 +324,12 @@ export class Iterable { Iterable?.logger?.log('getAttributionInfo'); return RNIterableAPI.getAttributionInfo().then( - (dict?: IterableAttributionInfo) => { + (dict: { campaignId: number; templateId: number; messageId: string } | null) => { if (dict) { return new IterableAttributionInfo( - dict.campaignId, - dict.templateId, - dict.messageId + dict.campaignId as number, + dict.templateId as number, + dict.messageId as string ); } else { return undefined; @@ -366,7 +365,7 @@ export class Iterable { static setAttributionInfo(attributionInfo?: IterableAttributionInfo) { Iterable?.logger?.log('setAttributionInfo'); - RNIterableAPI.setAttributionInfo(attributionInfo); + RNIterableAPI.setAttributionInfo(attributionInfo as unknown as { [key: string]: string | number | boolean; } | null); } /** @@ -410,9 +409,9 @@ export class Iterable { RNIterableAPI.trackPushOpenWithCampaignId( campaignId, templateId, - messageId, + messageId as string, appAlreadyRunning, - dataFields + dataFields as { [key: string]: string | number | boolean } | undefined ); } @@ -445,7 +444,7 @@ export class Iterable { static updateCart(items: IterableCommerceItem[]) { Iterable?.logger?.log('updateCart'); - RNIterableAPI.updateCart(items); + RNIterableAPI.updateCart(items as unknown as { [key: string]: string | number | boolean }[]); } /** @@ -497,7 +496,7 @@ export class Iterable { ) { Iterable?.logger?.log('trackPurchase'); - RNIterableAPI.trackPurchase(total, items, dataFields); + RNIterableAPI.trackPurchase(total, items as unknown as { [key: string]: string | number | boolean }[], dataFields as { [key: string]: string | number | boolean } | undefined); } /** @@ -666,7 +665,7 @@ export class Iterable { static trackEvent(name: string, dataFields?: unknown) { Iterable?.logger?.log('trackEvent'); - RNIterableAPI.trackEvent(name, dataFields); + RNIterableAPI.trackEvent(name, dataFields as { [key: string]: string | number | boolean } | undefined); } /** @@ -714,7 +713,7 @@ export class Iterable { ) { Iterable?.logger?.log('updateUser'); - RNIterableAPI.updateUser(dataFields, mergeNestedObjects); + RNIterableAPI.updateUser(dataFields as { [key: string]: string | number | boolean }, mergeNestedObjects); } /** @@ -859,10 +858,10 @@ export class Iterable { * ``` */ static updateSubscriptions( - emailListIds: number[] | undefined, - unsubscribedChannelIds: number[] | undefined, - unsubscribedMessageTypeIds: number[] | undefined, - subscribedMessageTypeIds: number[] | undefined, + emailListIds: number[] | null, + unsubscribedChannelIds: number[] | null, + unsubscribedMessageTypeIds: number[] | null, + subscribedMessageTypeIds: number[] | null, campaignId: number, templateId: number ) { diff --git a/src/core/classes/IterableAuthResponse.ts b/src/core/classes/IterableAuthResponse.ts index d071d4d23..29c1882f9 100644 --- a/src/core/classes/IterableAuthResponse.ts +++ b/src/core/classes/IterableAuthResponse.ts @@ -5,7 +5,7 @@ */ export class IterableAuthResponse { /** JWT Token */ - authToken?: string = ''; + authToken?: string | null = ''; /** Callback when the authentication to Iterable succeeds */ successCallback?: () => void; /** Callback when the authentication to Iterable fails */ diff --git a/src/inApp/classes/IterableInAppManager.ts b/src/inApp/classes/IterableInAppManager.ts index 640b99d50..03c99b9e5 100644 --- a/src/inApp/classes/IterableInAppManager.ts +++ b/src/inApp/classes/IterableInAppManager.ts @@ -1,5 +1,4 @@ -import { NativeModules } from 'react-native'; - +import { RNIterableAPI } from '../../api'; import { Iterable } from '../../core/classes/Iterable'; import type { IterableInAppDeleteSource, @@ -8,8 +7,6 @@ import type { import { IterableHtmlInAppContent } from './IterableHtmlInAppContent'; import { IterableInAppMessage } from './IterableInAppMessage'; -const RNIterableAPI = NativeModules.RNIterableAPI; - /** * Manages in-app messages for the current user. * @@ -38,7 +35,7 @@ export class IterableInAppManager { getMessages(): Promise { Iterable?.logger?.log('InAppManager.getMessages'); - return RNIterableAPI.getInAppMessages(); + return RNIterableAPI.getInAppMessages() as unknown as Promise; } /** @@ -61,7 +58,7 @@ export class IterableInAppManager { getInboxMessages(): Promise { Iterable?.logger?.log('InAppManager.getInboxMessages'); - return RNIterableAPI.getInboxMessages(); + return RNIterableAPI.getInboxMessages() as unknown as Promise; } /** @@ -85,7 +82,7 @@ export class IterableInAppManager { showMessage( message: IterableInAppMessage, consume: boolean - ): Promise { + ): Promise { Iterable?.logger?.log('InAppManager.show'); return RNIterableAPI.showMessage(message.messageId, consume); @@ -153,7 +150,7 @@ export class IterableInAppManager { ): Promise { Iterable?.logger?.log('InAppManager.getHtmlContentForMessage'); - return RNIterableAPI.getHtmlInAppContentForMessage(message.messageId); + return RNIterableAPI.getHtmlInAppContentForMessage(message.messageId) as unknown as Promise; } /** diff --git a/src/inbox/classes/IterableInboxDataModel.ts b/src/inbox/classes/IterableInboxDataModel.ts index fe5ce66fb..311f5cc7c 100644 --- a/src/inbox/classes/IterableInboxDataModel.ts +++ b/src/inbox/classes/IterableInboxDataModel.ts @@ -1,5 +1,4 @@ -import { NativeModules } from 'react-native'; - +import { RNIterableAPI } from '../../api'; import { Iterable } from '../../core/classes/Iterable'; import { IterableHtmlInAppContent, @@ -13,8 +12,6 @@ import type { IterableInboxRowViewModel, } from '../types'; -const RNIterableAPI = NativeModules.RNIterableAPI; - /** * The `IterableInboxDataModel` class provides methods to manage and manipulate * inbox messages. @@ -154,7 +151,7 @@ export class IterableInboxDataModel { * @param visibleRows - An array of `IterableInboxImpressionRowInfo` objects representing the rows that are currently visible. */ startSession(visibleRows: IterableInboxImpressionRowInfo[] = []) { - RNIterableAPI.startSession(visibleRows); + RNIterableAPI.startSession(visibleRows as unknown as { [key: string]: string | number | boolean }[]); } /** @@ -181,7 +178,7 @@ export class IterableInboxDataModel { * Defaults to an empty array if not provided. */ updateVisibleRows(visibleRows: IterableInboxImpressionRowInfo[] = []) { - RNIterableAPI.updateVisibleRows(visibleRows); + RNIterableAPI.updateVisibleRows(visibleRows as unknown as { [key: string]: string | number | boolean }[]); } /** diff --git a/src/inbox/components/IterableInbox.tsx b/src/inbox/components/IterableInbox.tsx index 545403e03..3cf44d829 100644 --- a/src/inbox/components/IterableInbox.tsx +++ b/src/inbox/components/IterableInbox.tsx @@ -3,7 +3,6 @@ import { useEffect, useState } from 'react'; import { Animated, NativeEventEmitter, - NativeModules, Platform, StyleSheet, Text, @@ -11,6 +10,7 @@ import { } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; +import RNIterableAPI from '../../api'; import { useAppStateListener, useDeviceOrientation } from '../../core'; // expo throws an error if this is not imported directly due to circular // dependencies @@ -32,7 +32,7 @@ import { type IterableInboxMessageListProps, } from './IterableInboxMessageList'; -const RNIterableAPI = NativeModules.RNIterableAPI; + const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); const DEFAULT_HEADLINE_HEIGHT = 60;