From a1559d8b85b38d2ed5801d978b71ac7858900559 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 24 Jul 2025 14:02:07 -0700 Subject: [PATCH 1/8] chore: update podspec for support for new architecture and swift, per upgrade instructions --- Iterable-React-Native-SDK.podspec | 40 +++++++++++-------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/Iterable-React-Native-SDK.podspec b/Iterable-React-Native-SDK.podspec index 14c6d5966..e074bb340 100644 --- a/Iterable-React-Native-SDK.podspec +++ b/Iterable-React-Native-SDK.podspec @@ -1,7 +1,6 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) -folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' Pod::Spec.new do |s| s.name = "Iterable-React-Native-SDK" @@ -14,31 +13,20 @@ Pod::Spec.new do |s| s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "https://github.com/Iterable/react-native-sdk.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm,swift}" - - # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. - # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. - if respond_to?(:install_modules_dependencies, true) - install_modules_dependencies(s) - else - s.dependency "React-Core" - - # Don't install the dependencies when we run `pod install` in the old architecture. - if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then - s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" - s.pod_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", - "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", - "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" - } - s.dependency "React-Codegen" - s.dependency "RCT-Folly" - s.dependency "RCTRequired" - s.dependency "RCTTypeSafety" - s.dependency "ReactCommon/turbomodule/core" - end - end + s.source_files = "ios/**/*.{h,m,mm,cpp,swift}" + s.private_header_files = "ios/**/*.h" + # Load Iterables iOS SDK as a dependency s.dependency "Iterable-iOS-SDK", "6.5.4" - + + # Basic Swift support + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'CLANG_ENABLE_MODULES' => 'YES', + 'SWIFT_VERSION' => '5.0', + "CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(), + } + + install_modules_dependencies(s) + end From 01d72b51edc475b1a14a5272e6277374171f3edb Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 24 Jul 2025 14:13:00 -0700 Subject: [PATCH 2/8] feat: add RNIterableAPI and NativeRNIterableAPI for improved native module integration --- package.json | 4 +- src/api/NativeRNIterableAPI.ts | 131 ++++++++++++++++++++ src/api/index.ts | 15 +++ src/core/classes/Iterable.ts | 5 +- src/global.d.ts | 3 + src/inApp/classes/IterableInAppManager.ts | 3 +- src/inbox/classes/IterableInboxDataModel.ts | 4 +- src/inbox/components/IterableInbox.tsx | 5 +- 8 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 src/api/NativeRNIterableAPI.ts create mode 100644 src/api/index.ts create mode 100644 src/global.d.ts 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..9c62a3934 --- /dev/null +++ b/src/api/NativeRNIterableAPI.ts @@ -0,0 +1,131 @@ +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 }, + version: string + ): Promise; + + initialize2WithApiKey( + apiKey: string, + config: { [key: string]: string | number | boolean }, + 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; + + // testing + testEventDispatch(): void; +} +export default TurboModuleRegistry.getEnforcing('RNIterableAPI'); diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 000000000..aee4b4e85 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports */ +import { NativeModules } from 'react-native'; + +export const isTurboModuleEnabled = global.__turboModuleProxy != null; + +const getNativeModule = () => { + if (isTurboModuleEnabled) { + return require('./NativeRNIterableAPI').default; + } + return NativeModules.RNIterableAPI; +}; + +export const RNIterableAPI = getNativeModule(); + +export default RNIterableAPI; diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 5b8b07fff..772373aa2 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -1,14 +1,14 @@ import { Linking, NativeEventEmitter, - NativeModules, - Platform, + Platform } from 'react-native'; import { buildInfo } from '../../itblBuildInfo'; // TODO: Organize these so that there are no circular dependencies // See https://github.com/expo/expo/issues/35100 +import { RNIterableAPI } from '../../api'; import { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; import { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; import { IterableInAppDeleteSource } from '../../inApp/enums/IterableInAppDeleteSource'; @@ -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 */ diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 000000000..3cd4060e3 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,3 @@ +declare const global: { + __turboModuleProxy?: (moduleName: string) => object | undefined; +}; diff --git a/src/inApp/classes/IterableInAppManager.ts b/src/inApp/classes/IterableInAppManager.ts index 640b99d50..0c7ac37cb 100644 --- a/src/inApp/classes/IterableInAppManager.ts +++ b/src/inApp/classes/IterableInAppManager.ts @@ -1,5 +1,5 @@ -import { NativeModules } from 'react-native'; +import { RNIterableAPI } from '../../api'; import { Iterable } from '../../core/classes/Iterable'; import type { IterableInAppDeleteSource, @@ -8,7 +8,6 @@ import type { import { IterableHtmlInAppContent } from './IterableHtmlInAppContent'; import { IterableInAppMessage } from './IterableInAppMessage'; -const RNIterableAPI = NativeModules.RNIterableAPI; /** * Manages in-app messages for the current user. diff --git a/src/inbox/classes/IterableInboxDataModel.ts b/src/inbox/classes/IterableInboxDataModel.ts index fe5ce66fb..a664a1b90 100644 --- a/src/inbox/classes/IterableInboxDataModel.ts +++ b/src/inbox/classes/IterableInboxDataModel.ts @@ -1,5 +1,5 @@ -import { NativeModules } from 'react-native'; +import { RNIterableAPI } from '../../api'; import { Iterable } from '../../core/classes/Iterable'; import { IterableHtmlInAppContent, @@ -8,12 +8,12 @@ import { IterableInAppMessage, type IterableHtmlInAppContentRaw, } from '../../inApp'; + import type { IterableInboxImpressionRowInfo, IterableInboxRowViewModel, } from '../types'; -const RNIterableAPI = NativeModules.RNIterableAPI; /** * The `IterableInboxDataModel` class provides methods to manage and manipulate diff --git a/src/inbox/components/IterableInbox.tsx b/src/inbox/components/IterableInbox.tsx index 545403e03..7e33b5969 100644 --- a/src/inbox/components/IterableInbox.tsx +++ b/src/inbox/components/IterableInbox.tsx @@ -3,14 +3,14 @@ import { useEffect, useState } from 'react'; import { Animated, NativeEventEmitter, - NativeModules, Platform, StyleSheet, Text, - View, + View } 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,6 @@ import { type IterableInboxMessageListProps, } from './IterableInboxMessageList'; -const RNIterableAPI = NativeModules.RNIterableAPI; const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); const DEFAULT_HEADLINE_HEIGHT = 60; From c60d74572d3274c7e13f2d02973c3ea50675cfeb Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 24 Jul 2025 14:42:32 -0700 Subject: [PATCH 3/8] refactor: update iOS native layers to use turbo modules --- .../project.pbxproj | 58 +-- ios/RNIterableAPI/RNIterableAPI.h | 21 +- ios/RNIterableAPI/RNIterableAPI.mm | 338 +++++++++---- ios/RNIterableAPI/ReactIterableAPI.swift | 449 +++++++++--------- ios/RNIterableAPI/Serialization.swift | 60 +-- 5 files changed, 542 insertions(+), 384 deletions(-) diff --git a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj index c6390b76b..72ef54fe8 100644 --- a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj +++ b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj @@ -9,10 +9,10 @@ /* Begin PBXBuildFile section */ 00E356F31AD99517003FC87E /* ReactNativeSdkExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* ReactNativeSdkExampleTests.m */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 33FDBABB81617A6933D0D172 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 518F52B1402DC88D14A95481 /* libPods-ReactNativeSdkExample.a */; }; 779227342DFA3FB500D69EC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779227332DFA3FB500D69EC0 /* AppDelegate.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; A3A40C20801B8F02005FA4C0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */; }; - D8AD8DD7C4BBDAA8AE397A1B /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1BA21919EC27D7F5BAE79A81 /* libPods-ReactNativeSdkExample.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -33,15 +33,15 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ReactNativeSdkExample/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ReactNativeSdkExample/Info.plist; sourceTree = ""; }; 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 1BA21919EC27D7F5BAE79A81 /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 24F802EFDCFB094D34916C72 /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; + 518F52B1402DC88D14A95481 /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 779227312DFA3FB500D69EC0 /* ReactNativeSdkExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExample-Bridging-Header.h"; sourceTree = ""; }; 779227322DFA3FB500D69EC0 /* ReactNativeSdkExampleTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExampleTests-Bridging-Header.h"; sourceTree = ""; }; 779227332DFA3FB500D69EC0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = ReactNativeSdkExample/AppDelegate.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReactNativeSdkExample/LaunchScreen.storyboard; sourceTree = ""; }; + ADEDFB551574301502EA4BE9 /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; + C4EA96511437C7B5CDC8C685 /* Pods-ReactNativeSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.debug.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; - EDCEA27594161CE66029771A /* Pods-ReactNativeSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -56,7 +56,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D8AD8DD7C4BBDAA8AE397A1B /* libPods-ReactNativeSdkExample.a in Frameworks */, + 33FDBABB81617A6933D0D172 /* libPods-ReactNativeSdkExample.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,7 +99,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 1BA21919EC27D7F5BAE79A81 /* libPods-ReactNativeSdkExample.a */, + 518F52B1402DC88D14A95481 /* libPods-ReactNativeSdkExample.a */, ); name = Frameworks; sourceTree = ""; @@ -138,8 +138,8 @@ BBD78D7AC51CEA395F1C20DB /* Pods */ = { isa = PBXGroup; children = ( - EDCEA27594161CE66029771A /* Pods-ReactNativeSdkExample.debug.xcconfig */, - 24F802EFDCFB094D34916C72 /* Pods-ReactNativeSdkExample.release.xcconfig */, + C4EA96511437C7B5CDC8C685 /* Pods-ReactNativeSdkExample.debug.xcconfig */, + ADEDFB551574301502EA4BE9 /* Pods-ReactNativeSdkExample.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -169,13 +169,13 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ReactNativeSdkExample" */; buildPhases = ( - F706883CA13F4E50A06B85B2 /* [CP] Check Pods Manifest.lock */, + AA1E5B9145571222A8F12852 /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - 2128D4591E26BD193F1293F1 /* [CP] Embed Pods Frameworks */, - BBD6908F2EAA6D3A6C078EA9 /* [CP] Copy Pods Resources */, + 4203829946B917334FC758FA /* [CP] Embed Pods Frameworks */, + F521D3718AAA25EC5D3903F9 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -260,7 +260,7 @@ shellPath = /bin/sh; shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; }; - 2128D4591E26BD193F1293F1 /* [CP] Embed Pods Frameworks */ = { + 4203829946B917334FC758FA /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -277,43 +277,43 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - BBD6908F2EAA6D3A6C078EA9 /* [CP] Copy Pods Resources */ = { + AA1E5B9145571222A8F12852 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Copy Pods Resources"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ReactNativeSdkExample-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - F706883CA13F4E50A06B85B2 /* [CP] Check Pods Manifest.lock */ = { + F521D3718AAA25EC5D3903F9 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Copy Pods Resources"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ReactNativeSdkExample-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -406,7 +406,7 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = EDCEA27594161CE66029771A /* Pods-ReactNativeSdkExample.debug.xcconfig */; + baseConfigurationReference = C4EA96511437C7B5CDC8C685 /* Pods-ReactNativeSdkExample.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -436,7 +436,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 24F802EFDCFB094D34916C72 /* Pods-ReactNativeSdkExample.release.xcconfig */; + baseConfigurationReference = ADEDFB551574301502EA4BE9 /* Pods-ReactNativeSdkExample.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; diff --git a/ios/RNIterableAPI/RNIterableAPI.h b/ios/RNIterableAPI/RNIterableAPI.h index 26bbf81fe..be3fee829 100644 --- a/ios/RNIterableAPI/RNIterableAPI.h +++ b/ios/RNIterableAPI/RNIterableAPI.h @@ -1,9 +1,16 @@ -// -// RNIterableAPI.h -// RNIterableAPI -// -// Created by Loren Posen on 6/11/25. -// Copyright © 2025 Iterable. All rights reserved. -// +#import +#import + +#if RCT_NEW_ARCH_ENABLED + +#import +@interface RNIterableAPI : RCTEventEmitter + +#else + #import +@interface RNIterableAPI : RCTEventEmitter + +#endif +@end diff --git a/ios/RNIterableAPI/RNIterableAPI.mm b/ios/RNIterableAPI/RNIterableAPI.mm index dc40a6e12..30b2610a1 100644 --- a/ios/RNIterableAPI/RNIterableAPI.mm +++ b/ios/RNIterableAPI/RNIterableAPI.mm @@ -1,139 +1,291 @@ -// -// Created by Tapash Majumder on 3/19/20. -// Copyright © 2020 Iterable. All rights reserved. -// #import "RNIterableAPI.h" -@interface RCT_EXTERN_REMAP_MODULE(RNIterableAPI, ReactIterableAPI, NSObject) +#if RCT_NEW_ARCH_ENABLED +#import "RNIterableAPISpec.h" +#endif -// MARK: - Native SDK Functions +#import // umbrella (Objective-C) header -RCT_EXTERN_METHOD(initializeWithApiKey: (nonnull NSString *) apiKey - config: (nonnull NSDictionary *) config - version: (nonnull NSString *) version - resolver: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) +// Forward-declare the Swift protocols/enum used in the Swift header. +@protocol IterableInAppDelegate; +@protocol IterableCustomActionDelegate; +@protocol IterableAuthDelegate; +@protocol IterableURLDelegate; +typedef NS_ENUM(NSInteger, InAppShowResponse) { + show = 0, + skip = 1, +}; -RCT_EXTERN_METHOD(initialize2WithApiKey: (nonnull NSString *) apiKey - config: (nonnull NSDictionary *) config - apiEndPointOverride: (nonnull NSString *) apiEndPoint - version: (nonnull NSString *) version - resolver: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) +#import "Iterable_React_Native_SDK-Swift.h" + +@interface RNIterableAPI () +@end + +@implementation RNIterableAPI { + ReactIterableAPI *_swiftAPI; +} -RCT_EXTERN_METHOD(setEmail: (NSString *) email - authToken: (NSString *) authToken) +#pragma mark - Init / bridge wiring -RCT_EXTERN_METHOD(getEmail: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) +- (instancetype)init { + self = [super init]; + if (self) { + _swiftAPI = [ReactIterableAPI new]; + _swiftAPI.delegate = self; + } + return self; +} -RCT_EXTERN_METHOD(setUserId: (NSString *) userId - authToken: (NSString *) authToken) +RCT_EXPORT_MODULE() -RCT_EXTERN_METHOD(getUserId: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) ++ (BOOL)requiresMainQueueSetup { + return NO; +} -// MARK: - Iterable API Request Functions +#pragma mark - TurboModule hook (new arch only) -RCT_EXTERN_METHOD(disableDeviceForCurrentUser) +#if RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} +#endif -RCT_EXTERN_METHOD(setInAppShowResponse: (nonnull NSNumber *) inAppShowResponse) +#pragma mark - Events -RCT_EXTERN_METHOD(getLastPushPayload: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) +- (NSArray *)supportedEvents { + return [ReactIterableAPI supportedEvents]; +} -RCT_EXTERN_METHOD(getAttributionInfo: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) +- (void)sendEventWithName:(NSString *_Nonnull)name result:(double)result { + [self sendEventWithName:name body:@(result)]; +} -RCT_EXTERN_METHOD(setAttributionInfo: (NSDictionary *) attributionInfo) +#pragma mark - Public API (all paths call into Swift) -RCT_EXTERN_METHOD(trackPushOpenWithCampaignId: (nonnull NSNumber *) campaignId +RCT_EXPORT_METHOD(testEventDispatch) { [_swiftAPI testEventDispatch]; } + +RCT_EXPORT_METHOD(initializeWithApiKey: (nonnull NSString *) apiKey + config: (nonnull NSDictionary *) config + version: (nonnull NSString *) version + resolver: (RCTPromiseResolveBlock) resolve + rejecter: (RCTPromiseRejectBlock) reject) { + [_swiftAPI initializeWithApiKey:apiKey + config:config + version:version + resolver:resolve + rejecter:reject]; +} + +RCT_EXPORT_METHOD(initialize2WithApiKey: (nonnull NSString *) apiKey + config: (nonnull NSDictionary *) config + apiEndPointOverride: (nonnull NSString *) apiEndPoint + version: (nonnull NSString *) version + resolver: (RCTPromiseResolveBlock) resolve + rejecter: (RCTPromiseRejectBlock) reject) { + [_swiftAPI initialize2WithApiKey:apiKey + config:config + apiEndPointOverride:apiEndPoint + version:version + resolver:resolve + rejecter:reject]; +} + +RCT_EXPORT_METHOD(setEmail: (NSString *) email + authToken: (NSString *) authToken) { + [_swiftAPI setEmail:email authToken:authToken]; +} + +RCT_EXPORT_METHOD(getEmail: (RCTPromiseResolveBlock) resolve + rejecter: (RCTPromiseRejectBlock) reject) { + [_swiftAPI getEmail:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(setUserId: (NSString *) userId + authToken: (NSString *) authToken) { + [_swiftAPI setUserId:userId authToken:authToken]; +} + +RCT_EXPORT_METHOD(getUserId: (RCTPromiseResolveBlock) resolve + rejecter: (RCTPromiseRejectBlock) reject) { + [_swiftAPI getUserId:resolve rejecter:reject]; +} + +#pragma mark - Iterable API Request Functions + +RCT_EXPORT_METHOD(disableDeviceForCurrentUser) { + [_swiftAPI disableDeviceForCurrentUser]; +} + +RCT_EXPORT_METHOD(setInAppShowResponse: (nonnull NSNumber *) inAppShowResponse) { + [_swiftAPI setInAppShowResponse:inAppShowResponse]; +} + +RCT_EXPORT_METHOD(getLastPushPayload: (RCTPromiseResolveBlock) resolve + rejecter: (RCTPromiseRejectBlock) reject) { + [_swiftAPI getLastPushPayload:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(getAttributionInfo : (RCTPromiseResolveBlock) + resolve reject : (RCTPromiseRejectBlock)reject) { + [_swiftAPI getAttributionInfo:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(setAttributionInfo: (NSDictionary *) attributionInfo) { + [_swiftAPI setAttributionInfo:attributionInfo]; +} + +RCT_EXPORT_METHOD(trackPushOpenWithCampaignId: (nonnull NSNumber *) campaignId templateId: (nonnull NSNumber *) templateId messageId: (nonnull NSString *) messageId appAlreadyRunning: (BOOL) appAlreadyRunning - dataFields: (NSDictionary *) dataFields) - -RCT_EXTERN_METHOD(updateCart: (NSArray *) items) - -RCT_EXTERN_METHOD(trackPurchase: (nonnull NSNumber *) total + dataFields: (NSDictionary *) dataFields) { + [_swiftAPI trackPushOpenWithCampaignId:campaignId + templateId:templateId + messageId:messageId + appAlreadyRunning:appAlreadyRunning + dataFields:dataFields]; +} + +RCT_EXPORT_METHOD(updateCart: (NSArray *) items) { + [_swiftAPI updateCart:items]; +} + +RCT_EXPORT_METHOD(trackPurchase: (nonnull NSNumber *) total items: (NSArray *) items - dataFields: (NSDictionary *) dataFields) + dataFields: (NSDictionary *) dataFields) { + [_swiftAPI trackPurchase:total items:items dataFields:dataFields]; +} -RCT_EXTERN_METHOD(trackInAppOpen: (NSString *) messageId - location: (nonnull NSNumber *) location) +RCT_EXPORT_METHOD(trackInAppOpen: (NSString *) messageId + location: (nonnull NSNumber *) location) { + [_swiftAPI trackInAppOpen:messageId location:location]; +} -RCT_EXTERN_METHOD(trackInAppClick: (nonnull NSString *) messageId +RCT_EXPORT_METHOD(trackInAppClick: (nonnull NSString *) messageId location: (nonnull NSNumber *) location - clickedUrl: (nonnull NSString *) clickedUrl) + clickedUrl: (nonnull NSString *) clickedUrl) { + [_swiftAPI trackInAppClick:messageId location:location clickedUrl:clickedUrl]; +} -RCT_EXTERN_METHOD(trackInAppClose: (nonnull NSString *) messageId +RCT_EXPORT_METHOD(trackInAppClose: (nonnull NSString *) messageId location: (nonnull NSNumber *) location source: (nonnull NSNumber *) source - clickedUrl: (NSString *) clickedUrl) - -RCT_EXTERN_METHOD(inAppConsume: (nonnull NSString *) messageId + clickedUrl: (NSString *) clickedUrl) { + [_swiftAPI trackInAppClose:messageId + location:location + source:source + clickedUrl:clickedUrl]; +} + +RCT_EXPORT_METHOD(inAppConsume: (nonnull NSString *) messageId location: (nonnull NSNumber *) location - source: (nonnull NSNumber *) source) - -RCT_EXTERN_METHOD(trackEvent: (nonnull NSString *) name - dataFields: (NSDictionary *) dataFields) - -RCT_EXTERN_METHOD(updateUser: (nonnull NSDictionary *) dataFields - mergeNestedObjects: (BOOL) mergeNestedObjects) - -RCT_EXTERN_METHOD(updateEmail: (nonnull NSString *) email - authToken: (NSString *) authToken) - -RCT_EXTERN_METHOD(handleAppLink: (nonnull NSString *) appLink + source: (nonnull NSNumber *) source) { + [_swiftAPI inAppConsume:messageId location:location source:source]; +} + +RCT_EXPORT_METHOD(trackEvent: (nonnull NSString *) name + dataFields: (NSDictionary *) dataFields) { + [_swiftAPI trackEvent:name dataFields:dataFields]; +} + +RCT_EXPORT_METHOD(updateUser: (nonnull NSDictionary *) dataFields + mergeNestedObjects: (BOOL) mergeNestedObjects) { + [_swiftAPI updateUser:dataFields mergeNestedObjects:mergeNestedObjects]; +} + +RCT_EXPORT_METHOD(updateEmail: (nonnull NSString *) email + authToken: (NSString *) authToken) { + [_swiftAPI updateEmail:email authToken:authToken]; +} + +RCT_EXPORT_METHOD(handleAppLink: (nonnull NSString *) appLink resolver: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) + rejecter: (RCTPromiseRejectBlock) reject) { + [_swiftAPI handleAppLink:appLink resolver:resolve rejecter:reject]; +} -RCT_EXTERN_METHOD(updateSubscriptions: (NSArray *) emailListIds +RCT_EXPORT_METHOD(updateSubscriptions: (NSArray *) emailListIds unsubscribedChannelIds: (NSArray *) unsubscribedChannelIds unsubscribedMessageTypeIds: (NSArray *) unsubscribedMessageTypeIds subscribedMessageTypeIds: (NSArray *) subscribedMessageTypeIds campaignId: (nonnull NSNumber *) campaignId - templateId: (nonnull NSNumber *) templateId) - -// MARK: - SDK In-App Manager Functions - -RCT_EXTERN_METHOD(getInAppMessages: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) - -RCT_EXTERN_METHOD(getHtmlInAppContentForMessage: (nonnull NSString *) messageId + templateId: (nonnull NSNumber *) templateId) { + [_swiftAPI updateSubscriptions:emailListIds + unsubscribedChannelIds:unsubscribedChannelIds + unsubscribedMessageTypeIds:unsubscribedMessageTypeIds + subscribedMessageTypeIds:subscribedMessageTypeIds + campaignId:campaignId + templateId:templateId]; +} + +#pragma mark - SDK In-App Manager Functions + +RCT_EXPORT_METHOD(getInAppMessages: (RCTPromiseResolveBlock) resolve + rejecter: (RCTPromiseRejectBlock) reject) { + [_swiftAPI getInAppMessages:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(getHtmlInAppContentForMessage: (nonnull NSString *) messageId resolver: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) - -RCT_EXTERN_METHOD(getInboxMessages: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) - -RCT_EXTERN_METHOD(getUnreadInboxMessagesCount: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) - -RCT_EXTERN_METHOD(showMessage: (nonnull NSString *) messageId - consume: (nonnull BOOL) consume + rejecter: (RCTPromiseRejectBlock) reject) { + [_swiftAPI getHtmlInAppContentForMessage:messageId + resolver:resolve + rejecter:reject]; +} + +RCT_EXPORT_METHOD(getInboxMessages: (RCTPromiseResolveBlock) resolve + rejecter: (RCTPromiseRejectBlock) reject) { + [_swiftAPI getInboxMessages:resolve rejecter:reject]; +} + +// NOTE: This is not used anywhere on the JS side. +RCT_EXPORT_METHOD(getUnreadInboxMessagesCount: (RCTPromiseResolveBlock) resolve + rejecter: (RCTPromiseRejectBlock) reject) { + [_swiftAPI getUnreadInboxMessagesCount:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(showMessage: (nonnull NSString *) messageId + consume: (BOOL) consume resolver: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) - -RCT_EXTERN_METHOD(removeMessage: (nonnull NSString *) messageId + rejecter: (RCTPromiseRejectBlock) reject) { + [_swiftAPI showMessage:messageId + consume:consume + resolver:resolve + rejecter:reject]; +} + +RCT_EXPORT_METHOD(removeMessage: (nonnull NSString *) messageId location: (nonnull NSNumber *) location - source: (nonnull NSNumber *) source) + source: (nonnull NSNumber *) source) { + [_swiftAPI removeMessage:messageId location:location source:source]; +} -RCT_EXTERN_METHOD(setReadForMessage: (nonnull NSString *) messageId - read: (BOOL) read) +RCT_EXPORT_METHOD(setReadForMessage: (nonnull NSString *) messageId + read: (BOOL) read) { + [_swiftAPI setReadForMessage:messageId read:read]; +} -RCT_EXTERN_METHOD(setAutoDisplayPaused: (BOOL) paused) +RCT_EXPORT_METHOD(setAutoDisplayPaused: (BOOL) paused) { + [_swiftAPI setAutoDisplayPaused:paused]; +} -// MARK: - SDK Inbox Session Tracking Functions +#pragma mark - SDK Inbox Session Tracking Functions -RCT_EXTERN_METHOD(startSession: (nonnull NSArray *) visibleRows) +RCT_EXPORT_METHOD(startSession: (nonnull NSArray *) visibleRows) { + [_swiftAPI startSession:visibleRows]; +} -RCT_EXTERN_METHOD(endSession) +RCT_EXPORT_METHOD(endSession) { [_swiftAPI endSession]; } -RCT_EXTERN_METHOD(updateVisibleRows: (nonnull NSArray *) visibleRows) +RCT_EXPORT_METHOD(updateVisibleRows: (nonnull NSArray *) visibleRows) { + [_swiftAPI updateVisibleRows:visibleRows]; +} -// MARK: - SDK Auth Manager Functions +#pragma mark - SDK Auth Manager Functions -RCT_EXTERN_METHOD(passAlongAuthToken: (NSString *) authToken) +RCT_EXPORT_METHOD(passAlongAuthToken: (NSString *) authToken) { + [_swiftAPI passAlongAuthToken:authToken]; +} @end diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index 4db314f20..6f3a1a095 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -1,32 +1,32 @@ -// -// Created by Tapash Majumder on 3/19/20. -// Copyright © 2020 Iterable. All rights reserved. -// - import Foundation - import IterableSDK +import React +@objc public protocol ReactIterableAPIDelegate { + func sendEvent(withName: String, body: Any?) +} @objc(ReactIterableAPI) -class ReactIterableAPI: RCTEventEmitter { +public class ReactIterableAPI: RCTEventEmitter { deinit { NotificationCenter.default.removeObserver(self) } - + + @objc public weak var delegate: ReactIterableAPIDelegate? = nil + // MARK: - React Native Functions - - @objc static override func moduleName() -> String! { + + @objc override public class func moduleName() -> String! { return "RNIterableAPI" } - - override var methodQueue: DispatchQueue! { + + override open var methodQueue: DispatchQueue! { _methodQueue } - - @objc override static func requiresMainQueueSetup() -> Bool { + + @objc override public static func requiresMainQueueSetup() -> Bool { false } - + enum EventName: String, CaseIterable { case handleUrlCalled case handleCustomActionCalled @@ -36,55 +36,54 @@ class ReactIterableAPI: RCTEventEmitter { case handleAuthSuccessCalled case handleAuthFailureCalled } - - override func supportedEvents() -> [String]! { - var result = [String]() - - EventName.allCases.forEach { - result.append($0.rawValue) - } - - return result + + @objc public static var supportedEvents: [String] { + return EventName.allCases.map(\.rawValue) } - - override func startObserving() { + + override public func startObserving() { ITBInfo() - + shouldEmit = true } - - override func stopObserving() { + + override public func stopObserving() { ITBInfo() - + shouldEmit = false } - + + @objc(testEventDispatch) + public func testEventDispatch() { + delegate?.sendEvent(withName: EventName.onTestEventDispatch.rawValue, body: 0) + } + // MARK: - Native SDK Functions - + @objc(initializeWithApiKey:config:version:resolver:rejecter:) - func initialize(apiKey: String, - config configDict: [AnyHashable: Any], + public func initialize(apiKey: String, + config configDict: NSDictionary, version: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { ITBInfo() - + initialize(withApiKey: apiKey, config: configDict, version: version, resolver: resolver, rejecter: rejecter) } - + @objc(initialize2WithApiKey:config:apiEndPointOverride:version:resolver:rejecter:) - func initialize2(apiKey: String, - config configDict: [AnyHashable: Any], + public func initialize2(apiKey: String, + config configDict: NSDictionary, version: String, apiEndPointOverride: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { ITBInfo() - + initialize(withApiKey: apiKey, config: configDict, version: version, @@ -92,165 +91,165 @@ class ReactIterableAPI: RCTEventEmitter { resolver: resolver, rejecter: rejecter) } - + @objc(setEmail:) - func set(email: String?) { + public func set(email: String?) { ITBInfo() - + IterableAPI.email = email } @objc(setEmail:authToken:) - func set(email: String?, authToken: String?) { + public func set(email: String?, authToken: String?) { ITBInfo() IterableAPI.setEmail(email, authToken) } - + @objc(getEmail:rejecter:) - func getEmail(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + public func getEmail(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { ITBInfo() - + resolver(IterableAPI.email) } - + @objc(setUserId:) - func set(userId: String?) { + public func set(userId: String?) { ITBInfo() - + IterableAPI.userId = userId } @objc(setUserId:authToken:) - func set(userId: String?, authToken: String?) { + public func set(userId: String?, authToken: String?) { ITBInfo() - + IterableAPI.setUserId(userId, authToken) } - + @objc(getUserId:rejecter:) - func getUserId(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + public func getUserId(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { ITBInfo() - + resolver(IterableAPI.userId) } - + // MARK: - Iterable API Request Functions - + @objc(setInAppShowResponse:) - func set(inAppShowResponse number: NSNumber) { + public func set(inAppShowResponse number: NSNumber) { ITBInfo() - + self.inAppShowResponse = InAppShowResponse.from(number: number) - + inAppHandlerSemaphore.signal() } - + @objc(disableDeviceForCurrentUser) - func disableDeviceForCurrentUser() { + public func disableDeviceForCurrentUser() { ITBInfo() - + IterableAPI.disableDeviceForCurrentUser() } - + @objc(getLastPushPayload:rejecter:) - func getLastPushPayload(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + public func getLastPushPayload(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { ITBInfo() - + resolver(IterableAPI.lastPushPayload) } - + @objc(getAttributionInfo:rejecter:) - func getAttributionInfo(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + public func getAttributionInfo(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { ITBInfo() - + resolver(IterableAPI.attributionInfo.map(SerializationUtil.encodableToDictionary)) } - + @objc(setAttributionInfo:) - func set(attributionInfo dict: [AnyHashable: Any]?) { + public func set(attributionInfo dict: NSDictionary?) { ITBInfo() - + guard let dict = dict else { IterableAPI.attributionInfo = nil return } - - IterableAPI.attributionInfo = SerializationUtil.dictionaryToDecodable(dict: dict) + + IterableAPI.attributionInfo = SerializationUtil.dictionaryToDecodable(dict: dict as! [AnyHashable: Any]) } - + @objc(trackPushOpenWithCampaignId:templateId:messageId:appAlreadyRunning:dataFields:) - func trackPushOpen(campaignId: NSNumber, + public func trackPushOpen(campaignId: NSNumber, templateId: NSNumber?, messageId: String, appAlreadyRunning: Bool, - dataFields: [AnyHashable: Any]?) { + dataFields: NSDictionary?) { ITBInfo() - + IterableAPI.track(pushOpen: campaignId, templateId: templateId, messageId: messageId, appAlreadyRunning: appAlreadyRunning, - dataFields: dataFields) + dataFields: dataFields as? [AnyHashable: Any]) } @objc(updateCart:) - func updateCart(items: [[AnyHashable: Any]]) { + public func updateCart(items: [[AnyHashable: Any]]) { ITBInfo() IterableAPI.updateCart(items: items.compactMap(CommerceItem.from(dict:))) } - + @objc(trackPurchase:items:dataFields:) - func trackPurchase(total: NSNumber, + public func trackPurchase(total: NSNumber, items: [[AnyHashable: Any]], dataFields: [AnyHashable: Any]?) { ITBInfo() - + IterableAPI.track(purchase: total, items: items.compactMap(CommerceItem.from(dict:)), dataFields: dataFields) } - + @objc(trackInAppOpen:location:) - func trackInAppOpen(messageId: String, + public func trackInAppOpen(messageId: String, location locationNumber: NSNumber) { ITBInfo() - + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { ITBError("Could not find message with id: \(messageId)") return } - + IterableAPI.track(inAppOpen: message, location: InAppLocation.from(number: locationNumber)) } - + @objc(trackInAppClick:location:clickedUrl:) - func trackInAppClick(messageId: String, + public func trackInAppClick(messageId: String, location locationNumber: NSNumber, clickedUrl: String) { ITBInfo() - + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { ITBError("Could not find message with id: \(messageId)") return } - + IterableAPI.track(inAppClick: message, location: InAppLocation.from(number: locationNumber), clickedUrl: clickedUrl) } - + @objc(trackInAppClose:location:source:clickedUrl:) - func trackInAppClose(messageId: String, + public func trackInAppClose(messageId: String, location locationNumber: NSNumber, source sourceNumber: NSNumber, clickedUrl: String?) { ITBInfo() - + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { ITBError("Could not find message with id: \(messageId)") return } - + if let inAppCloseSource = InAppCloseSource.from(number: sourceNumber) { IterableAPI.track(inAppClose: message, location: InAppLocation.from(number: locationNumber), @@ -262,18 +261,18 @@ class ReactIterableAPI: RCTEventEmitter { clickedUrl: clickedUrl) } } - + @objc(inAppConsume:location:source:) - func inAppConsume(messageId: String, + public func inAppConsume(messageId: String, location locationNumber: NSNumber, source sourceNumber: NSNumber) { ITBInfo() - + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { ITBError("Could not find message with id: \(messageId)") return } - + if let inAppDeleteSource = InAppDeleteSource.from(number: sourceNumber) { IterableAPI.inAppConsume(message: message, location: InAppLocation.from(number: locationNumber), @@ -283,108 +282,108 @@ class ReactIterableAPI: RCTEventEmitter { location: InAppLocation.from(number: locationNumber)) } } - + @objc(getHtmlInAppContentForMessage:resolver:rejecter:) - func getHtmlInAppContent(messageId: String, resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + public func getHtmlInAppContent(messageId: String, resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { ITBInfo() - + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { ITBError("Could not find message with id: \(messageId)") rejecter("", "Could not find message with id: \(messageId)", nil) return } - + guard let content = message.content as? IterableHtmlInAppContent else { ITBError("Could not parse message content as HTML") rejecter("", "Could not parse message content as HTML", nil) return } - + resolver(content.toDict()) } - + @objc(trackEvent:dataFields:) - func trackEvent(name: String, dataFields: [AnyHashable: Any]?) { + public func trackEvent(name: String, dataFields: NSDictionary?) { ITBInfo() - - IterableAPI.track(event: name, dataFields: dataFields) + + IterableAPI.track(event: name, dataFields: dataFields as? [AnyHashable: Any]) } - + @objc(updateUser:mergeNestedObjects:) - func updateUser(dataFields: [AnyHashable: Any], mergeNestedObjects: Bool) { + public func updateUser(dataFields: NSDictionary, mergeNestedObjects: Bool) { ITBInfo() - - IterableAPI.updateUser(dataFields, mergeNestedObjects: mergeNestedObjects) + + IterableAPI.updateUser(dataFields as? [AnyHashable: Any], mergeNestedObjects: mergeNestedObjects) } - + @objc(updateEmail:authToken:) - func updateEmail(email: String, with authToken: String?) { + public func updateEmail(email: String, with authToken: String?) { ITBInfo() - + if let authToken = authToken { IterableAPI.updateEmail(email, withToken: authToken, onSuccess: nil, onFailure: nil) } else { IterableAPI.updateEmail(email, onSuccess: nil, onFailure: nil) } } - + @objc(handleAppLink:resolver:rejecter:) - func handle(appLink: String, resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + public func handle(appLink: String, resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { ITBInfo() - + if let url = URL(string: appLink) { resolver(IterableAPI.handle(universalLink: url)) } else { rejecter("", "invalid URL", nil) } } - + // MARK: - SDK In-App Manager Functions - + @objc(getInAppMessages:rejecter:) - func getInAppMessages(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + public func getInAppMessages(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { ITBInfo() - + resolver(IterableAPI.inAppManager.getMessages().map { $0.toDict() }) } - + @objc(getInboxMessages:rejecter:) - func getInboxMessages(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + public func getInboxMessages(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { ITBInfo() - + resolver(IterableAPI.inAppManager.getInboxMessages().map{ $0.toDict() }) } - + @objc(getUnreadInboxMessagesCount:rejecter:) - func getUnreadInboxMessagesCount(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + public func getUnreadInboxMessagesCount(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { ITBInfo() - + resolver(IterableAPI.inAppManager.getUnreadInboxMessagesCount()) } - + @objc(showMessage:consume:resolver:rejecter:) - func show(messageId: String, consume: Bool, resolver: @escaping RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + public func show(messageId: String, consume: Bool, resolver: @escaping RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { ITBInfo() - + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { ITBError("Could not find message with id: \(messageId)") return } - + IterableAPI.inAppManager.show(message: message, consume: consume) { (url) in resolver(url.map({$0.absoluteString})) } } - + @objc(removeMessage:location:source:) - func remove(messageId: String, location locationNumber: NSNumber, source sourceNumber: NSNumber) { + public func remove(messageId: String, location locationNumber: NSNumber, source sourceNumber: NSNumber) { ITBInfo() - + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { ITBError("Could not find message with id: \(messageId)") return } - + if let inAppDeleteSource = InAppDeleteSource.from(number: sourceNumber) { IterableAPI.inAppManager.remove(message: message, location: InAppLocation.from(number: locationNumber), @@ -394,19 +393,19 @@ class ReactIterableAPI: RCTEventEmitter { location: InAppLocation.from(number: locationNumber)) } } - + @objc(updateSubscriptions:unsubscribedChannelIds:unsubscribedMessageTypeIds:subscribedMessageTypeIds:campaignId:templateId:) - func updateSubscriptions(emailListIds: [NSNumber]?, + public func updateSubscriptions(emailListIds: [NSNumber]?, unsubscribedChannelIds: [NSNumber]?, unsubscribedMessageTypeIds: [NSNumber]?, subscribedMessageTypeIds: [NSNumber]?, campaignId: NSNumber, templateId: NSNumber) { ITBInfo() - + let finalCampaignId: NSNumber? = campaignId.intValue <= 0 ? nil : campaignId let finalTemplateId: NSNumber? = templateId.intValue <= 0 ? nil : templateId - + IterableAPI.updateSubscriptions(emailListIds, unsubscribedChannelIds: unsubscribedChannelIds, unsubscribedMessageTypeIds: unsubscribedMessageTypeIds, @@ -414,44 +413,44 @@ class ReactIterableAPI: RCTEventEmitter { campaignId: finalCampaignId, templateId: finalTemplateId) } - + @objc(setReadForMessage:read:) - func setRead(for messageId: String, read: Bool) { + public func setRead(for messageId: String, read: Bool) { ITBInfo() - + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { ITBError("Could not find message with id: \(messageId)") return } - + IterableAPI.inAppManager.set(read: read, forMessage: message) } - + @objc(setAutoDisplayPaused:) - func set(autoDisplayPaused: Bool) { + public func set(autoDisplayPaused: Bool) { ITBInfo() - + DispatchQueue.main.async { IterableAPI.inAppManager.isAutoDisplayPaused = autoDisplayPaused } } - + // MARK: - SDK Inbox Session Tracking Functions - + @objc(startSession:) - func startSession(visibleRows: [[AnyHashable: Any]]) { + public func startSession(visibleRows: [[AnyHashable: Any]]) { let serializedRows = InboxImpressionTracker.RowInfo.rowInfos(from: visibleRows) - + inboxSessionManager.startSession(visibleRows: serializedRows) } - + @objc(endSession) - func endSession() { + public func endSession() { guard let sessionInfo = inboxSessionManager.endSession() else { ITBError("Could not find session info") return } - + let inboxSession = IterableInboxSession(id: sessionInfo.startInfo.id, sessionStartTime: sessionInfo.startInfo.startTime, sessionEndTime: Date(), @@ -460,71 +459,71 @@ class ReactIterableAPI: RCTEventEmitter { endTotalMessageCount: IterableAPI.inAppManager.getInboxMessages().count, endUnreadMessageCount: IterableAPI.inAppManager.getUnreadInboxMessagesCount(), impressions: sessionInfo.impressions.map { $0.toIterableInboxImpression() }) - + IterableAPI.track(inboxSession: inboxSession) } - + @objc(updateVisibleRows:) - func updateVisibleRows(visibleRows: [[AnyHashable: Any]]) { + public func updateVisibleRows(visibleRows: [NSDictionary]) { let serializedRows = InboxImpressionTracker.RowInfo.rowInfos(from: visibleRows) - + inboxSessionManager.updateVisibleRows(visibleRows: serializedRows) } - + // MARK: - SDK Auth Manager Functions - + @objc(passAlongAuthToken:) - func passAlong(authToken: String?) { + public func passAlong(authToken: String?) { ITBInfo() - + passedAuthToken = authToken - + authHandlerSemaphore.signal() } - + // MARK: Private private var shouldEmit = false private let _methodQueue = DispatchQueue(label: String(describing: ReactIterableAPI.self)) - + // Handling in-app delegate private var inAppShowResponse = InAppShowResponse.show private var inAppHandlerSemaphore = DispatchSemaphore(value: 0) - + private var passedAuthToken: String? private var authHandlerSemaphore = DispatchSemaphore(value: 0) - + private let inboxSessionManager = InboxSessionManager() - - private func initialize(withApiKey apiKey: String, - config configDict: [AnyHashable: Any], + + @objc func initialize(withApiKey apiKey: String, + config configDict: NSDictionary, version: String, apiEndPointOverride: String? = nil, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { ITBInfo() - + let launchOptions = createLaunchOptions() let iterableConfig = IterableConfig.from(dict: configDict) - + if let urlHandlerPresent = configDict["urlHandlerPresent"] as? Bool, urlHandlerPresent == true { iterableConfig.urlDelegate = self } - + if let customActionHandlerPresent = configDict["customActionHandlerPresent"] as? Bool, customActionHandlerPresent == true { iterableConfig.customActionDelegate = self } - + if let inAppHandlerPresent = configDict["inAppHandlerPresent"] as? Bool, inAppHandlerPresent == true { iterableConfig.inAppDelegate = self } - + if let authHandlerPresent = configDict["authHandlerPresent"] as? Bool, authHandlerPresent { iterableConfig.authDelegate = self } - + // connect new inbox in-app payloads to the RN SDK NotificationCenter.default.addObserver(self, selector: #selector(receivedIterableInboxChanged), name: Notification.Name.iterableInboxChanged, object: nil) - + DispatchQueue.main.async { IterableAPI.initialize2(apiKey: apiKey, launchOptions: launchOptions, @@ -532,112 +531,112 @@ class ReactIterableAPI: RCTEventEmitter { apiEndPointOverride: apiEndPointOverride) { result in resolver(result) } - + IterableAPI.setDeviceAttribute(name: "reactNativeSDKVersion", value: version) } } - + @objc(receivedIterableInboxChanged) - private func receivedIterableInboxChanged() { + public func receivedIterableInboxChanged() { guard shouldEmit else { return } - - sendEvent(withName: EventName.receivedIterableInboxChanged.rawValue, body: nil) + + delegate?.sendEvent(withName: EventName.receivedIterableInboxChanged.rawValue, body: nil) } - + private func createLaunchOptions() -> [UIApplication.LaunchOptionsKey: Any]? { - guard let bridge = bridge else { + guard let bridge = self.bridge else { return nil } - + return ReactIterableAPI.createLaunchOptions(bridgeLaunchOptions: bridge.launchOptions) } - + private static func createLaunchOptions(bridgeLaunchOptions: [AnyHashable: Any]?) -> [UIApplication.LaunchOptionsKey: Any]? { guard let bridgeLaunchOptions = bridgeLaunchOptions, let remoteNotification = bridgeLaunchOptions[UIApplication.LaunchOptionsKey.remoteNotification.rawValue] else { return nil } - + var result = [UIApplication.LaunchOptionsKey: Any]() result[UIApplication.LaunchOptionsKey.remoteNotification] = remoteNotification - + return result } } extension ReactIterableAPI: IterableURLDelegate { - func handle(iterableURL url: URL, inContext context: IterableActionContext) -> Bool { + public func handle(iterableURL url: URL, inContext context: IterableActionContext) -> Bool { ITBInfo() - + guard shouldEmit else { return false } - + let contextDict = ReactIterableAPI.contextToDictionary(context: context) - sendEvent(withName: EventName.handleUrlCalled.rawValue, + delegate?.sendEvent(withName: EventName.handleUrlCalled.rawValue, body: ["url": url.absoluteString, "context": contextDict] as [String : Any]) - + return true } - + private static func contextToDictionary(context: IterableActionContext) -> [AnyHashable: Any] { var result = [AnyHashable: Any]() - + let actionDict = actionToDictionary(action: context.action) result["action"] = actionDict result["source"] = context.source.rawValue - + return result } - + private static func actionToDictionary(action: IterableAction) -> [AnyHashable: Any] { var actionDict = [AnyHashable: Any]() - + actionDict["type"] = action.type - + if let data = action.data { actionDict["data"] = data } - + if let userInput = action.userInput { actionDict["userInput"] = userInput } - + return actionDict } } extension ReactIterableAPI: IterableCustomActionDelegate { - func handle(iterableCustomAction action: IterableAction, inContext context: IterableActionContext) -> Bool { + public func handle(iterableCustomAction action: IterableAction, inContext context: IterableActionContext) -> Bool { ITBInfo() - + let actionDict = ReactIterableAPI.actionToDictionary(action: action) let contextDict = ReactIterableAPI.contextToDictionary(context: context) - - sendEvent(withName: EventName.handleCustomActionCalled.rawValue, + + delegate?.sendEvent(withName: EventName.handleCustomActionCalled.rawValue, body: ["action": actionDict, "context": contextDict]) - + return true } } extension ReactIterableAPI: IterableInAppDelegate { - func onNew(message: IterableInAppMessage) -> InAppShowResponse { + public func onNew(message: IterableInAppMessage) -> InAppShowResponse { ITBInfo() - + guard shouldEmit else { return .show } - - sendEvent(withName: EventName.handleInAppCalled.rawValue, + + delegate?.sendEvent(withName: EventName.handleInAppCalled.rawValue, body: message.toDict()) - + let timeoutResult = inAppHandlerSemaphore.wait(timeout: .now() + 2.0) - + if timeoutResult == .success { ITBInfo("inAppShowResponse: \(inAppShowResponse == .show)") return inAppShowResponse @@ -649,38 +648,38 @@ extension ReactIterableAPI: IterableInAppDelegate { } extension ReactIterableAPI: IterableAuthDelegate { - func onAuthTokenRequested(completion: @escaping AuthTokenRetrievalHandler) { + public func onAuthTokenRequested(completion: @escaping AuthTokenRetrievalHandler) { ITBInfo() - + DispatchQueue.global(qos: .userInitiated).async { self.sendEvent(withName: EventName.handleAuthCalled.rawValue, - body: nil) - + body: nil as Any?) + let authTokenRetrievalResult = self.authHandlerSemaphore.wait(timeout: .now() + 30.0) - + if authTokenRetrievalResult == .success { ITBInfo("authTokenRetrieval successful") - + DispatchQueue.main.async { completion(self.passedAuthToken) } - + self.sendEvent(withName: EventName.handleAuthSuccessCalled.rawValue, - body: nil) + body: nil as Any?) } else { ITBInfo("authTokenRetrieval timed out") - + DispatchQueue.main.async { completion(nil) } - + self.sendEvent(withName: EventName.handleAuthFailureCalled.rawValue, - body: nil) + body: nil as Any?) } } } - - func onTokenRegistrationFailed(_ reason: String?) { - + + public func onTokenRegistrationFailed(_ reason: String?) { + } } diff --git a/ios/RNIterableAPI/Serialization.swift b/ios/RNIterableAPI/Serialization.swift index cb27026be..8e37eb0c6 100644 --- a/ios/RNIterableAPI/Serialization.swift +++ b/ios/RNIterableAPI/Serialization.swift @@ -11,42 +11,42 @@ struct SerializationUtil { static func dateToInt(date: Date) -> Int { Int(date.timeIntervalSince1970 * 1000) } - + static func intToDate(int: Int) -> Date { let seconds = Double(int) / 1000.0 // ms -> seconds - + return Date(timeIntervalSince1970: seconds) } - + static func encodableToDictionary(encodable: T) -> [String: Any]? where T: Encodable { guard let data = try? JSONEncoder().encode(encodable) else { return nil } - + return try? JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String: Any] } - + static func dictionaryToDecodable(dict: [AnyHashable: Any]) -> T? where T: Decodable { guard let data = try? JSONSerialization.data(withJSONObject: dict, options: []) else { return nil } - + return try? JSONDecoder().decode(T.self, from: data) } } extension IterableConfig { - static func from(dict: [AnyHashable: Any]?) -> IterableConfig { + static func from(dict: NSDictionary?) -> IterableConfig { let config = IterableConfig() - + guard let dict = dict else { return config } - + if let allowedProtocols = dict["allowedProtocols"] as? [String] { config.allowedProtocols = allowedProtocols } - + if let pushIntegrationName = dict["pushIntegrationName"] as? String { config.pushIntegrationName = pushIntegrationName } @@ -61,15 +61,15 @@ extension IterableConfig { config.pushPlatform = .auto } } - + if let autoPushRegistration = dict["autoPushRegistration"] as? Bool { config.autoPushRegistration = autoPushRegistration } - + if let inAppDisplayInterval = dict["inAppDisplayInterval"] as? Double { config.inAppDisplayInterval = inAppDisplayInterval } - + if let expiringAuthTokenRefreshPeriod = dict["expiringAuthTokenRefreshPeriod"] as? TimeInterval { config.expiringAuthTokenRefreshPeriod = expiringAuthTokenRefreshPeriod } @@ -77,7 +77,7 @@ extension IterableConfig { if let logLevelNumber = dict["logLevel"] as? NSNumber { config.logDelegate = createLogDelegate(logLevelNumber: logLevelNumber) } - + if let useInMemoryStorageForInApp = dict["useInMemoryStorageForInApps"] as? Bool { config.useInMemoryStorageForInApps = useInMemoryStorageForInApp } @@ -92,11 +92,11 @@ extension IterableConfig { config.dataRegion = IterableDataRegion.US } } - - + + return config } - + private static func createLogDelegate(logLevelNumber: NSNumber) -> IterableLogDelegate { DefaultLogDelegate(minLogLevel: LogLevel.from(number: logLevelNumber)) } @@ -107,26 +107,26 @@ extension CommerceItem { guard let id = dict["id"] as? String else { return nil } - + guard let name = dict["name"] as? String else { return nil } - + guard let price = dict["price"] as? NSNumber else { return nil } - + guard let quantity = dict["quantity"] as? UInt else { return nil } - + let sku = dict["sku"] as? String let description = dict["description"] as? String let url = dict["url"] as? String let imageUrl = dict["imageUrl"] as? String let categories = dict["categories"] as? [String] let dataFields = dict["dataFields"] as? [AnyHashable: Any] - + return CommerceItem(id: id, name: name, price: price, @@ -182,7 +182,7 @@ extension IterableInAppMessage { dict["customPayload"] = customPayload dict["read"] = read dict["priorityLevel"] = priorityLevel - + return dict } } @@ -212,7 +212,7 @@ extension InAppCloseSource { guard let value = number as? Int else { return nil } - + return InAppCloseSource(rawValue: value) } } @@ -222,7 +222,7 @@ extension InAppDeleteSource { guard let value = number as? Int else { return nil } - + return InAppDeleteSource(rawValue: value) } } @@ -248,20 +248,20 @@ extension LogLevel { } extension InboxImpressionTracker.RowInfo { - static func from(dict: [AnyHashable: Any]) -> InboxImpressionTracker.RowInfo? { + static func from(dict: NSDictionary) -> InboxImpressionTracker.RowInfo? { guard let messageId = dict["messageId"] as? String else { return nil } - + guard let silentInbox = dict["silentInbox"] as? Bool else { return nil } - + let rowInfo = InboxImpressionTracker.RowInfo(messageId: messageId, silentInbox: silentInbox) - + return rowInfo } - + static func rowInfos(from rows: [[AnyHashable: Any]]) -> [InboxImpressionTracker.RowInfo] { return rows.compactMap(InboxImpressionTracker.RowInfo.from(dict:)) } From 50d0cf4369395a6d1d174bf0f170d37f0cabfac9 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 24 Jul 2025 15:39:45 -0700 Subject: [PATCH 4/8] feat: added mode to package and altered types in swift so that they work with cpp --- example/ios/Podfile | 2 +- .../project.pbxproj | 58 +-- ios/RNIterableAPI/RNIterableAPI.mm | 339 +++++++++--------- ios/RNIterableAPI/ReactIterableAPI.swift | 31 +- ios/RNIterableAPI/Serialization.swift | 4 +- package.json | 6 + 6 files changed, 227 insertions(+), 213 deletions(-) diff --git a/example/ios/Podfile b/example/ios/Podfile index 833bd46c8..42978901b 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -13,7 +13,7 @@ prepare_react_native_project! linkage = ENV['USE_FRAMEWORKS'] if linkage != nil Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green - use_frameworks! :linkage => linkage.to_sym + use_frameworks! :linkage => :dynamic end target 'ReactNativeSdkExample' do diff --git a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj index 72ef54fe8..32fc4c132 100644 --- a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj +++ b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj @@ -9,10 +9,10 @@ /* Begin PBXBuildFile section */ 00E356F31AD99517003FC87E /* ReactNativeSdkExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* ReactNativeSdkExampleTests.m */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; - 33FDBABB81617A6933D0D172 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 518F52B1402DC88D14A95481 /* libPods-ReactNativeSdkExample.a */; }; 779227342DFA3FB500D69EC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779227332DFA3FB500D69EC0 /* AppDelegate.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; A3A40C20801B8F02005FA4C0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */; }; + D676F724AF631EA8784FCCAD /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1BF8B855D40C338BCF805B23 /* libPods-ReactNativeSdkExample.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -33,14 +33,14 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ReactNativeSdkExample/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ReactNativeSdkExample/Info.plist; sourceTree = ""; }; 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 1BF8B855D40C338BCF805B23 /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 518F52B1402DC88D14A95481 /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5ADA50384391946ED83C750E /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; 779227312DFA3FB500D69EC0 /* ReactNativeSdkExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExample-Bridging-Header.h"; sourceTree = ""; }; 779227322DFA3FB500D69EC0 /* ReactNativeSdkExampleTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExampleTests-Bridging-Header.h"; sourceTree = ""; }; 779227332DFA3FB500D69EC0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = ReactNativeSdkExample/AppDelegate.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReactNativeSdkExample/LaunchScreen.storyboard; sourceTree = ""; }; - ADEDFB551574301502EA4BE9 /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; - C4EA96511437C7B5CDC8C685 /* Pods-ReactNativeSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.debug.xcconfig"; sourceTree = ""; }; + 90DCEC4520E54C6A13BB5FDC /* Pods-ReactNativeSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.debug.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -56,7 +56,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 33FDBABB81617A6933D0D172 /* libPods-ReactNativeSdkExample.a in Frameworks */, + D676F724AF631EA8784FCCAD /* libPods-ReactNativeSdkExample.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,7 +99,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 518F52B1402DC88D14A95481 /* libPods-ReactNativeSdkExample.a */, + 1BF8B855D40C338BCF805B23 /* libPods-ReactNativeSdkExample.a */, ); name = Frameworks; sourceTree = ""; @@ -138,8 +138,8 @@ BBD78D7AC51CEA395F1C20DB /* Pods */ = { isa = PBXGroup; children = ( - C4EA96511437C7B5CDC8C685 /* Pods-ReactNativeSdkExample.debug.xcconfig */, - ADEDFB551574301502EA4BE9 /* Pods-ReactNativeSdkExample.release.xcconfig */, + 90DCEC4520E54C6A13BB5FDC /* Pods-ReactNativeSdkExample.debug.xcconfig */, + 5ADA50384391946ED83C750E /* Pods-ReactNativeSdkExample.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -169,13 +169,13 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ReactNativeSdkExample" */; buildPhases = ( - AA1E5B9145571222A8F12852 /* [CP] Check Pods Manifest.lock */, + 981F53D791F0E4FE8BCDF7DB /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - 4203829946B917334FC758FA /* [CP] Embed Pods Frameworks */, - F521D3718AAA25EC5D3903F9 /* [CP] Copy Pods Resources */, + CA49B9E1CC35BBC17B422B31 /* [CP] Embed Pods Frameworks */, + CEDF0C37F91A1E965073EBEC /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -260,46 +260,46 @@ shellPath = /bin/sh; shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; }; - 4203829946B917334FC758FA /* [CP] Embed Pods Frameworks */ = { + 981F53D791F0E4FE8BCDF7DB /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ReactNativeSdkExample-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - AA1E5B9145571222A8F12852 /* [CP] Check Pods Manifest.lock */ = { + CA49B9E1CC35BBC17B422B31 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ReactNativeSdkExample-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - F521D3718AAA25EC5D3903F9 /* [CP] Copy Pods Resources */ = { + CEDF0C37F91A1E965073EBEC /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -406,7 +406,7 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C4EA96511437C7B5CDC8C685 /* Pods-ReactNativeSdkExample.debug.xcconfig */; + baseConfigurationReference = 90DCEC4520E54C6A13BB5FDC /* Pods-ReactNativeSdkExample.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -436,7 +436,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = ADEDFB551574301502EA4BE9 /* Pods-ReactNativeSdkExample.release.xcconfig */; + baseConfigurationReference = 5ADA50384391946ED83C750E /* Pods-ReactNativeSdkExample.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; diff --git a/ios/RNIterableAPI/RNIterableAPI.mm b/ios/RNIterableAPI/RNIterableAPI.mm index 30b2610a1..bef0a765b 100644 --- a/ios/RNIterableAPI/RNIterableAPI.mm +++ b/ios/RNIterableAPI/RNIterableAPI.mm @@ -1,10 +1,6 @@ #import "RNIterableAPI.h" - -#if RCT_NEW_ARCH_ENABLED #import "RNIterableAPISpec.h" -#endif - -#import // umbrella (Objective-C) header +#import // umbrella (Objective-C) header // Forward-declare the Swift protocols/enum used in the Swift header. @protocol IterableInAppDelegate; @@ -21,15 +17,15 @@ typedef NS_ENUM(NSInteger, InAppShowResponse) { @interface RNIterableAPI () @end + @implementation RNIterableAPI { ReactIterableAPI *_swiftAPI; } -#pragma mark - Init / bridge wiring - - (instancetype)init { self = [super init]; - if (self) { + if(self) { + // Option 2.B - Instantiate the Calculator and set the delegate _swiftAPI = [ReactIterableAPI new]; _swiftAPI.delegate = self; } @@ -38,254 +34,257 @@ - (instancetype)init { RCT_EXPORT_MODULE() -+ (BOOL)requiresMainQueueSetup { - return NO; -} - -#pragma mark - TurboModule hook (new arch only) - -#if RCT_NEW_ARCH_ENABLED -- (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params { - return std::make_shared(params); -} -#endif - -#pragma mark - Events - - (NSArray *)supportedEvents { return [ReactIterableAPI supportedEvents]; } -- (void)sendEventWithName:(NSString *_Nonnull)name result:(double)result { +- (void)sendEventWithName:(NSString * _Nonnull)name result:(double)result { [self sendEventWithName:name body:@(result)]; } -#pragma mark - Public API (all paths call into Swift) - -RCT_EXPORT_METHOD(testEventDispatch) { [_swiftAPI testEventDispatch]; } +- (void)testEventDispatch { + [_swiftAPI testEventDispatch]; +} -RCT_EXPORT_METHOD(initializeWithApiKey: (nonnull NSString *) apiKey - config: (nonnull NSDictionary *) config - version: (nonnull NSString *) version - resolver: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) { +- (void)initializeWithApiKey:(NSString *)apiKey + config:(NSDictionary *)config + version:(NSString *)version + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ [_swiftAPI initializeWithApiKey:apiKey - config:config - version:version - resolver:resolve - rejecter:reject]; -} - -RCT_EXPORT_METHOD(initialize2WithApiKey: (nonnull NSString *) apiKey - config: (nonnull NSDictionary *) config - apiEndPointOverride: (nonnull NSString *) apiEndPoint - version: (nonnull NSString *) version - resolver: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) { + config:config + version:version + resolver:resolve + rejecter:reject]; +} + +- (void)initialize2WithApiKey:(NSString *)apiKey + config:(NSDictionary *)config + version:(NSString *)version + apiEndPointOverride:(NSString *)apiEndPointOverride + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ [_swiftAPI initialize2WithApiKey:apiKey config:config - apiEndPointOverride:apiEndPoint + apiEndPointOverride:apiEndPointOverride version:version resolver:resolve rejecter:reject]; } -RCT_EXPORT_METHOD(setEmail: (NSString *) email - authToken: (NSString *) authToken) { +- (void)setEmail:(NSString * _Nullable)email + authToken:(NSString * _Nullable)authToken +{ [_swiftAPI setEmail:email authToken:authToken]; } -RCT_EXPORT_METHOD(getEmail: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) { +- (void)getEmail:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ [_swiftAPI getEmail:resolve rejecter:reject]; } -RCT_EXPORT_METHOD(setUserId: (NSString *) userId - authToken: (NSString *) authToken) { +- (void)setUserId:(NSString * _Nullable)userId + authToken:(NSString * _Nullable)authToken +{ [_swiftAPI setUserId:userId authToken:authToken]; } -RCT_EXPORT_METHOD(getUserId: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) { +- (void)getUserId:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ [_swiftAPI getUserId:resolve rejecter:reject]; } -#pragma mark - Iterable API Request Functions - -RCT_EXPORT_METHOD(disableDeviceForCurrentUser) { - [_swiftAPI disableDeviceForCurrentUser]; -} - -RCT_EXPORT_METHOD(setInAppShowResponse: (nonnull NSNumber *) inAppShowResponse) { +- (void)setInAppShowResponse:(NSNumber *)inAppShowResponse +{ [_swiftAPI setInAppShowResponse:inAppShowResponse]; } -RCT_EXPORT_METHOD(getLastPushPayload: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) { - [_swiftAPI getLastPushPayload:resolve rejecter:reject]; +- (void)getInAppMessages:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + [_swiftAPI getInAppMessages:resolve rejecter:reject]; } -RCT_EXPORT_METHOD(getAttributionInfo : (RCTPromiseResolveBlock) - resolve reject : (RCTPromiseRejectBlock)reject) { - [_swiftAPI getAttributionInfo:resolve rejecter:reject]; +- (void)getInboxMessages:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + [_swiftAPI getInboxMessages:resolve rejecter:reject]; } -RCT_EXPORT_METHOD(setAttributionInfo: (NSDictionary *) attributionInfo) { - [_swiftAPI setAttributionInfo:attributionInfo]; +// NOTE: This is not used anywhere on the JS side. +- (void)getUnreadInboxMessagesCount:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + [_swiftAPI getUnreadInboxMessagesCount:resolve rejecter:reject]; } -RCT_EXPORT_METHOD(trackPushOpenWithCampaignId: (nonnull NSNumber *) campaignId - templateId: (nonnull NSNumber *) templateId - messageId: (nonnull NSString *) messageId - appAlreadyRunning: (BOOL) appAlreadyRunning - dataFields: (NSDictionary *) dataFields) { - [_swiftAPI trackPushOpenWithCampaignId:campaignId - templateId:templateId - messageId:messageId - appAlreadyRunning:appAlreadyRunning - dataFields:dataFields]; +- (void)showMessage:(NSString *)messageId + consume:(BOOL)consume + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + [_swiftAPI showMessage:messageId consume:consume resolver:resolve rejecter:reject]; } -RCT_EXPORT_METHOD(updateCart: (NSArray *) items) { - [_swiftAPI updateCart:items]; +- (void)removeMessage:(NSString *)messageId + location:(NSNumber *)location + source:(NSNumber *)source +{ + [_swiftAPI removeMessage:messageId location:location source:source]; } -RCT_EXPORT_METHOD(trackPurchase: (nonnull NSNumber *) total - items: (NSArray *) items - dataFields: (NSDictionary *) dataFields) { - [_swiftAPI trackPurchase:total items:items dataFields:dataFields]; +- (void)setReadForMessage:(NSString *)messageId + read:(BOOL)read +{ + [_swiftAPI setReadForMessage:messageId read:read]; } -RCT_EXPORT_METHOD(trackInAppOpen: (NSString *) messageId - location: (nonnull NSNumber *) location) { - [_swiftAPI trackInAppOpen:messageId location:location]; +- (void)setAutoDisplayPaused:(BOOL)autoDisplayPaused +{ + [_swiftAPI setAutoDisplayPaused:autoDisplayPaused]; } -RCT_EXPORT_METHOD(trackInAppClick: (nonnull NSString *) messageId - location: (nonnull NSNumber *) location - clickedUrl: (nonnull NSString *) clickedUrl) { - [_swiftAPI trackInAppClick:messageId location:location clickedUrl:clickedUrl]; +- (void)trackEvent:(NSString *)name + dataFields:(NSDictionary *)dataFields +{ + [_swiftAPI trackEvent:name dataFields:dataFields]; } -RCT_EXPORT_METHOD(trackInAppClose: (nonnull NSString *) messageId - location: (nonnull NSNumber *) location - source: (nonnull NSNumber *) source - clickedUrl: (NSString *) clickedUrl) { - [_swiftAPI trackInAppClose:messageId - location:location - source:source - clickedUrl:clickedUrl]; +- (void)trackPushOpenWithCampaignId:(NSNumber *)campaignId + templateId:(NSNumber *)templateId + messageId:(NSString *)messageId + appAlreadyRunning:(BOOL)appAlreadyRunning + dataFields:(NSDictionary *)dataFields +{ + [_swiftAPI trackPushOpenWithCampaignId:campaignId templateId:templateId messageId:messageId appAlreadyRunning:appAlreadyRunning dataFields:dataFields]; } -RCT_EXPORT_METHOD(inAppConsume: (nonnull NSString *) messageId - location: (nonnull NSNumber *) location - source: (nonnull NSNumber *) source) { - [_swiftAPI inAppConsume:messageId location:location source:source]; +- (void)trackInAppOpen:(NSString *)messageId + location:(NSNumber *)location +{ + [_swiftAPI trackInAppOpen:messageId location:location]; } -RCT_EXPORT_METHOD(trackEvent: (nonnull NSString *) name - dataFields: (NSDictionary *) dataFields) { - [_swiftAPI trackEvent:name dataFields:dataFields]; +- (void)trackInAppClick:(NSString *)messageId + location:(NSNumber *)location + clickedUrl:(NSString *)clickedUrl +{ + [_swiftAPI trackInAppClick:messageId location:location clickedUrl:clickedUrl]; } -RCT_EXPORT_METHOD(updateUser: (nonnull NSDictionary *) dataFields - mergeNestedObjects: (BOOL) mergeNestedObjects) { - [_swiftAPI updateUser:dataFields mergeNestedObjects:mergeNestedObjects]; +- (void)trackInAppClose:(NSString *)messageId + location:(NSNumber *)location + source:(NSNumber *)source + clickedUrl:(NSString *)clickedUrl +{ + [_swiftAPI trackInAppClose:messageId location:location source:source clickedUrl:clickedUrl]; } -RCT_EXPORT_METHOD(updateEmail: (nonnull NSString *) email - authToken: (NSString *) authToken) { - [_swiftAPI updateEmail:email authToken:authToken]; +- (void)inAppConsume:(NSString *)messageId + location:(NSNumber *)location + source:(NSNumber *)source +{ + [_swiftAPI inAppConsume:messageId location:location source:source]; } -RCT_EXPORT_METHOD(handleAppLink: (nonnull NSString *) appLink - resolver: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) { - [_swiftAPI handleAppLink:appLink resolver:resolve rejecter:reject]; +- (void)updateCart:(NSArray *)items +{ + [_swiftAPI updateCart:items]; } -RCT_EXPORT_METHOD(updateSubscriptions: (NSArray *) emailListIds - unsubscribedChannelIds: (NSArray *) unsubscribedChannelIds - unsubscribedMessageTypeIds: (NSArray *) unsubscribedMessageTypeIds - subscribedMessageTypeIds: (NSArray *) subscribedMessageTypeIds - campaignId: (nonnull NSNumber *) campaignId - templateId: (nonnull NSNumber *) templateId) { - [_swiftAPI updateSubscriptions:emailListIds - unsubscribedChannelIds:unsubscribedChannelIds - unsubscribedMessageTypeIds:unsubscribedMessageTypeIds - subscribedMessageTypeIds:subscribedMessageTypeIds - campaignId:campaignId - templateId:templateId]; +- (void)trackPurchase:(NSNumber *)total + items:(NSArray *)items + dataFields:(NSDictionary *)dataFields +{ + [_swiftAPI trackPurchase:total items:items dataFields:dataFields]; } -#pragma mark - SDK In-App Manager Functions - -RCT_EXPORT_METHOD(getInAppMessages: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) { - [_swiftAPI getInAppMessages:resolve rejecter:reject]; +- (void)updateUser:(NSDictionary *)dataFields + mergeNestedObjects:(BOOL)mergeNestedObjects +{ + [_swiftAPI updateUser:dataFields mergeNestedObjects:mergeNestedObjects]; } -RCT_EXPORT_METHOD(getHtmlInAppContentForMessage: (nonnull NSString *) messageId - resolver: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) { - [_swiftAPI getHtmlInAppContentForMessage:messageId - resolver:resolve - rejecter:reject]; +- (void)updateEmail:(NSString *)email + authToken:(NSString *)authToken +{ + [_swiftAPI updateEmail:email authToken:authToken]; } -RCT_EXPORT_METHOD(getInboxMessages: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) { - [_swiftAPI getInboxMessages:resolve rejecter:reject]; +- (void)getAttributionInfo:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + [_swiftAPI getAttributionInfo:resolve rejecter:reject]; } -// NOTE: This is not used anywhere on the JS side. -RCT_EXPORT_METHOD(getUnreadInboxMessagesCount: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) { - [_swiftAPI getUnreadInboxMessagesCount:resolve rejecter:reject]; +- (void)setAttributionInfo:(NSDictionary *)attributionInfo +{ + [_swiftAPI setAttributionInfo:attributionInfo]; } -RCT_EXPORT_METHOD(showMessage: (nonnull NSString *) messageId - consume: (BOOL) consume - resolver: (RCTPromiseResolveBlock) resolve - rejecter: (RCTPromiseRejectBlock) reject) { - [_swiftAPI showMessage:messageId - consume:consume - resolver:resolve - rejecter:reject]; +- (void)disableDeviceForCurrentUser +{ + [_swiftAPI disableDeviceForCurrentUser]; } -RCT_EXPORT_METHOD(removeMessage: (nonnull NSString *) messageId - location: (nonnull NSNumber *) location - source: (nonnull NSNumber *) source) { - [_swiftAPI removeMessage:messageId location:location source:source]; +- (void)getLastPushPayload:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + [_swiftAPI getLastPushPayload:resolve rejecter:reject]; } -RCT_EXPORT_METHOD(setReadForMessage: (nonnull NSString *) messageId - read: (BOOL) read) { - [_swiftAPI setReadForMessage:messageId read:read]; +- (void)getHtmlInAppContentForMessage:(NSString *)messageId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + [_swiftAPI getHtmlInAppContentForMessage:messageId resolver:resolve rejecter:reject]; } -RCT_EXPORT_METHOD(setAutoDisplayPaused: (BOOL) paused) { - [_swiftAPI setAutoDisplayPaused:paused]; +- (void)handleAppLink:(NSString *)appLink + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + [_swiftAPI handleAppLink:appLink resolver:resolve rejecter:reject]; } -#pragma mark - SDK Inbox Session Tracking Functions +- (void)updateSubscriptions:(NSArray *)emailListIds + unsubscribedChannelIds:(NSArray *)unsubscribedChannelIds + unsubscribedMessageTypeIds:(NSArray *)unsubscribedMessageTypeIds + subscribedMessageTypeIds:(NSArray *)subscribedMessageTypeIds + campaignId:(NSNumber *)campaignId + templateId:(NSNumber *)templateId +{ + [_swiftAPI updateSubscriptions:emailListIds unsubscribedChannelIds:unsubscribedChannelIds unsubscribedMessageTypeIds:unsubscribedMessageTypeIds subscribedMessageTypeIds:subscribedMessageTypeIds campaignId:campaignId templateId:templateId]; +} -RCT_EXPORT_METHOD(startSession: (nonnull NSArray *) visibleRows) { +- (void)startSession:(NSArray *)visibleRows +{ [_swiftAPI startSession:visibleRows]; } -RCT_EXPORT_METHOD(endSession) { [_swiftAPI endSession]; } +- (void)endSession +{ + [_swiftAPI endSession]; +} -RCT_EXPORT_METHOD(updateVisibleRows: (nonnull NSArray *) visibleRows) { +- (void)updateVisibleRows:(NSArray *)visibleRows +{ [_swiftAPI updateVisibleRows:visibleRows]; } -#pragma mark - SDK Auth Manager Functions - -RCT_EXPORT_METHOD(passAlongAuthToken: (NSString *) authToken) { +- (void)passAlongAuthToken:(NSString *)authToken +{ [_swiftAPI passAlongAuthToken:authToken]; } +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + @end diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index 6f3a1a095..f70c8b997 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -35,6 +35,7 @@ public class ReactIterableAPI: RCTEventEmitter { case receivedIterableInboxChanged case handleAuthSuccessCalled case handleAuthFailureCalled + case onTestEventDispatch } @objc public static var supportedEvents: [String] { @@ -194,21 +195,23 @@ public class ReactIterableAPI: RCTEventEmitter { } @objc(updateCart:) - public func updateCart(items: [[AnyHashable: Any]]) { + public func updateCart(items: [NSDictionary]) { ITBInfo() - IterableAPI.updateCart(items: items.compactMap(CommerceItem.from(dict:))) + let swiftItems = items.compactMap { $0 as? [AnyHashable: Any] } + IterableAPI.updateCart(items: swiftItems.compactMap(CommerceItem.from(dict:))) } @objc(trackPurchase:items:dataFields:) public func trackPurchase(total: NSNumber, - items: [[AnyHashable: Any]], - dataFields: [AnyHashable: Any]?) { + items: [NSDictionary], + dataFields: NSDictionary?) { ITBInfo() + let swiftItems = items.compactMap { $0 as? [AnyHashable: Any] } IterableAPI.track(purchase: total, - items: items.compactMap(CommerceItem.from(dict:)), - dataFields: dataFields) + items: swiftItems.compactMap(CommerceItem.from(dict:)), + dataFields: dataFields as? [AnyHashable: Any]) } @objc(trackInAppOpen:location:) @@ -313,7 +316,11 @@ public class ReactIterableAPI: RCTEventEmitter { public func updateUser(dataFields: NSDictionary, mergeNestedObjects: Bool) { ITBInfo() - IterableAPI.updateUser(dataFields as? [AnyHashable: Any], mergeNestedObjects: mergeNestedObjects) + IterableAPI + .updateUser( + dataFields as! [AnyHashable: Any], + mergeNestedObjects: mergeNestedObjects + ) } @objc(updateEmail:authToken:) @@ -438,8 +445,8 @@ public class ReactIterableAPI: RCTEventEmitter { // MARK: - SDK Inbox Session Tracking Functions @objc(startSession:) - public func startSession(visibleRows: [[AnyHashable: Any]]) { - let serializedRows = InboxImpressionTracker.RowInfo.rowInfos(from: visibleRows) + public func startSession(visibleRows: [NSDictionary]) { + let serializedRows = InboxImpressionTracker.RowInfo.rowInfos(from: visibleRows as! [[AnyHashable: Any]]) inboxSessionManager.startSession(visibleRows: serializedRows) } @@ -465,7 +472,7 @@ public class ReactIterableAPI: RCTEventEmitter { @objc(updateVisibleRows:) public func updateVisibleRows(visibleRows: [NSDictionary]) { - let serializedRows = InboxImpressionTracker.RowInfo.rowInfos(from: visibleRows) + let serializedRows = InboxImpressionTracker.RowInfo.rowInfos(from: visibleRows as! [[AnyHashable: Any]]) inboxSessionManager.updateVisibleRows(visibleRows: serializedRows) } @@ -503,7 +510,9 @@ public class ReactIterableAPI: RCTEventEmitter { ITBInfo() let launchOptions = createLaunchOptions() - let iterableConfig = IterableConfig.from(dict: configDict) + let iterableConfig = IterableConfig.from( + dict: configDict as? [AnyHashable: Any] + ) if let urlHandlerPresent = configDict["urlHandlerPresent"] as? Bool, urlHandlerPresent == true { iterableConfig.urlDelegate = self diff --git a/ios/RNIterableAPI/Serialization.swift b/ios/RNIterableAPI/Serialization.swift index 8e37eb0c6..6ab712e39 100644 --- a/ios/RNIterableAPI/Serialization.swift +++ b/ios/RNIterableAPI/Serialization.swift @@ -36,7 +36,7 @@ struct SerializationUtil { } extension IterableConfig { - static func from(dict: NSDictionary?) -> IterableConfig { + static func from(dict: [AnyHashable: Any]?) -> IterableConfig { let config = IterableConfig() guard let dict = dict else { @@ -248,7 +248,7 @@ extension LogLevel { } extension InboxImpressionTracker.RowInfo { - static func from(dict: NSDictionary) -> InboxImpressionTracker.RowInfo? { + static func from(dict: [AnyHashable: Any]) -> InboxImpressionTracker.RowInfo? { guard let messageId = dict["messageId"] as? String else { return nil } diff --git a/package.json b/package.json index fb9752664..ebfd25a20 100644 --- a/package.json +++ b/package.json @@ -182,6 +182,12 @@ "jsSrcsDir": "src/api/", "android": { "javaPackageName": "com.iterable.reactnative" + }, + "ios": { + "modules": { + "RNIterableAPI": "RNIterableAPI", + "ReactIterableAPI": "ReactIterableAPI" + } } }, "create-react-native-library": { From 251f6f699fe41530df976e956e4f52b0cc0b7f8a Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Sat, 26 Jul 2025 13:08:39 -0700 Subject: [PATCH 5/8] feat: events sending in new arch --- EventDispatcher.swift | 28 ++ EventEmitterHostLegacy.h | 3 + EventEmitterHostLegacy.mm | 4 + EventEmitterHostLegacy.swift | 48 ++++ EventEmitterHostNew.swift | 50 ++++ RNIterableAPI+Module.swift | 0 example/ios/NotificationExt/Info.plist | 13 + .../NotificationExt/NotificationService.swift | 40 +++ example/ios/Podfile | 6 + .../project.pbxproj | 261 ++++++++++++++++-- .../ReactNativeSdkExample/AppDelegate.swift | 126 ++++++++- example/ios/ReactNativeSdkExample/Info.plist | 39 +-- .../ReactNativeSdkExample.entitlements | 10 + example/src/components/App/App.constants.ts | 1 + example/src/components/App/Main.tsx | 8 + .../src/components/Utility/Utility.styles.ts | 17 ++ example/src/components/Utility/Utility.tsx | 76 +++++ example/src/components/Utility/index.ts | 2 + example/src/constants/routes.ts | 1 + example/src/types/navigation.ts | 1 + ios/RNIterableAPI/RNIterableAPI.h | 14 +- ios/RNIterableAPI/RNIterableAPI.mm | 69 ++++- ios/RNIterableAPI/ReactIterableAPI.swift | 173 ++++++++---- src/inbox/components/IterableInbox.tsx | 1 + src/index.tsx | 1 + 25 files changed, 876 insertions(+), 116 deletions(-) create mode 100644 EventDispatcher.swift create mode 100644 EventEmitterHostLegacy.h create mode 100644 EventEmitterHostLegacy.mm create mode 100644 EventEmitterHostLegacy.swift create mode 100644 EventEmitterHostNew.swift create mode 100644 RNIterableAPI+Module.swift create mode 100644 example/ios/NotificationExt/Info.plist create mode 100644 example/ios/NotificationExt/NotificationService.swift create mode 100644 example/ios/ReactNativeSdkExample/ReactNativeSdkExample.entitlements create mode 100644 example/src/components/Utility/Utility.styles.ts create mode 100644 example/src/components/Utility/Utility.tsx create mode 100644 example/src/components/Utility/index.ts diff --git a/EventDispatcher.swift b/EventDispatcher.swift new file mode 100644 index 000000000..fc59e204d --- /dev/null +++ b/EventDispatcher.swift @@ -0,0 +1,28 @@ +// class EventDispatcherAnother { +// private var legacyEmitter: EventEmitterHostLegacy? +// private var bridgelessEmitter: EventEmitterHostNew? + +// init(bridge: RCTBridge) { +// NSLog("*** LEGACY EventDispatcher init ***") +// self.legacyEmitter = bridge.module(for: EventEmitterHostLegacy.self) as? EventEmitterHostLegacy +// } + +// init(appContext: RCTAppContext) { +// NSLog("*** BRIDGELESS EventDispatcher init ***") +// self.bridgelessEmitter = EventEmitterHostNew(appContext: appContext) +// } + +// func send(name: String, body: Any?) { +// if let emitter = legacyEmitter { +// emitter.send(name: name, body: body) +// } else if let emitter = bridgelessEmitter { +// emitter.send(name: name, body: body) +// } else { +// NSLog("⚠️ No emitter available to send event: \(name)") +// } +// } + +// func getShouldEmit() -> Bool { +// return legacyEmitter?.getShouldEmit() ?? bridgelessEmitter?.getShouldEmit() ?? false +// } +// } diff --git a/EventEmitterHostLegacy.h b/EventEmitterHostLegacy.h new file mode 100644 index 000000000..4b826ed1d --- /dev/null +++ b/EventEmitterHostLegacy.h @@ -0,0 +1,3 @@ +// #import +// @interface EventEmitterHostLegacy : RCTEventEmitter +// @end diff --git a/EventEmitterHostLegacy.mm b/EventEmitterHostLegacy.mm new file mode 100644 index 000000000..6675f64a9 --- /dev/null +++ b/EventEmitterHostLegacy.mm @@ -0,0 +1,4 @@ +// #import "EventEmitterHostLegacy.h" +// @implementation EventEmitterHostLegacy +// RCT_EXPORT_MODULE() +// @end diff --git a/EventEmitterHostLegacy.swift b/EventEmitterHostLegacy.swift new file mode 100644 index 000000000..0f05cf20c --- /dev/null +++ b/EventEmitterHostLegacy.swift @@ -0,0 +1,48 @@ +// // EventEmitterHost.swift +// import Foundation +// import React + +// @objc class EventEmitterHostLegacy: RCTEventEmitter { +// static let shared = EventEmitterHost() + +// private var shouldEmit = false + +// override init() { +// super.init() +// NSLog("*** EventEmitterHost initialized ***") +// } + +// override class func moduleName() -> String! { +// return "EventEmitterHost" +// } + +// override func supportedEvents() -> [String] { +// return ReactIterableAPI.EventName.allCases.map(\.rawValue) +// } + +// override class func requiresMainQueueSetup() -> Bool { +// return false +// } + +// override func startObserving() { +// ITBInfo() +// shouldEmit = true +// } + +// override func stopObserving() { +// ITBInfo() +// shouldEmit = false +// } + +// func send(name: String, body: Any?) { +// guard shouldEmit else { +// NSLog("[EventEmitterHost] Skipping emit: \(name), no listeners") +// return +// } +// sendEvent(withName: name, body: body) +// } + +// func getShouldEmit() -> Bool { +// return shouldEmit +// } +// } diff --git a/EventEmitterHostNew.swift b/EventEmitterHostNew.swift new file mode 100644 index 000000000..283c870ff --- /dev/null +++ b/EventEmitterHostNew.swift @@ -0,0 +1,50 @@ +// import Foundation +// import React + +// @objc(EventEmitterHostNew) +// class EventEmitterHostNew: NSObject { +// private let appContext: RCTAppContext + +// init(appContext: RCTAppContext) { +// self.appContext = appContext +// super.init() +// NSLog("*** EventEmitterHostNew initialized ***") +// } + +// func send(name: String, body: Any?) { +// appContext.emit(eventName: name, payload: body) +// } + +// func getShouldEmit() -> Bool { +// // Optional logic if you want to control flow +// return true +// } +// } + + + + +// // class EventEmitterHostNew { +// // private var legacyEmitter: LegacyEventEmitter? +// // private var bridgelessEmitter: BridgelessEventEmitter? + +// // init(bridge: RCTBridge) { +// // NSLog("*** LEGACY EventDispatcher init ***") +// // self.legacyEmitter = bridge.module(for: LegacyEventEmitter.self) as? LegacyEventEmitter +// // } + +// // init(appContext: RCTAppContext) { +// // NSLog("*** BRIDGELESS EventDispatcher init ***") +// // self.bridgelessEmitter = BridgelessEventEmitter(appContext: appContext) +// // } + +// // func send(name: String, body: Any?) { +// // if let emitter = legacyEmitter { +// // emitter.send(name, body: body) +// // } else if let emitter = bridgelessEmitter { +// // emitter.send(name, body: body) +// // } else { +// // NSLog("⚠️ No event emitter available to send event: \(name)") +// // } +// // } +// // } diff --git a/RNIterableAPI+Module.swift b/RNIterableAPI+Module.swift new file mode 100644 index 000000000..e69de29bb diff --git a/example/ios/NotificationExt/Info.plist b/example/ios/NotificationExt/Info.plist new file mode 100644 index 000000000..57421ebf9 --- /dev/null +++ b/example/ios/NotificationExt/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/example/ios/NotificationExt/NotificationService.swift b/example/ios/NotificationExt/NotificationService.swift new file mode 100644 index 000000000..d6cac6f1e --- /dev/null +++ b/example/ios/NotificationExt/NotificationService.swift @@ -0,0 +1,40 @@ +// // +// // NotificationService.swift +// // NotificationExt +// // +// // Created by Loren Posen on 7/24/25. +// // + +// import UserNotifications + +// class NotificationService: UNNotificationServiceExtension { + +// var contentHandler: ((UNNotificationContent) -> Void)? +// var bestAttemptContent: UNMutableNotificationContent? + +// override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { +// self.contentHandler = contentHandler +// bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) + +// if let bestAttemptContent = bestAttemptContent { +// // Modify the notification content here... +// bestAttemptContent.title = "\(bestAttemptContent.title) [modified]" + +// contentHandler(bestAttemptContent) +// } +// } + +// override func serviceExtensionTimeWillExpire() { +// // Called just before the extension will be terminated by the system. +// // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. +// if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { +// contentHandler(bestAttemptContent) +// } +// } + +// } + +import UserNotifications +import IterableAppExtensions + +class NotificationService: ITBNotificationServiceExtension { } diff --git a/example/ios/Podfile b/example/ios/Podfile index 42978901b..8b0cd0fb3 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -13,6 +13,8 @@ prepare_react_native_project! linkage = ENV['USE_FRAMEWORKS'] if linkage != nil Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green + # IMPORTANT: New Architecture Issue solution + # This is needed to use the Swift code from Iterable-iOS-SDK in the RNIterableAPI module use_frameworks! :linkage => :dynamic end @@ -35,3 +37,7 @@ target 'ReactNativeSdkExample' do ) end end + +target 'NotificationExt' do + pod 'Iterable-iOS-AppExtensions' +end diff --git a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj index 32fc4c132..14f2b3d55 100644 --- a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj +++ b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj @@ -9,10 +9,13 @@ /* Begin PBXBuildFile section */ 00E356F31AD99517003FC87E /* ReactNativeSdkExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* ReactNativeSdkExampleTests.m */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 65AD4F06C28F65CB4E898F9A /* libPods-NotificationExt.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FA966B65909B06BFEB95FCC9 /* libPods-NotificationExt.a */; }; 779227342DFA3FB500D69EC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779227332DFA3FB500D69EC0 /* AppDelegate.swift */; }; + 77DB813A2E330329004FEDA8 /* NotificationExt.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 77DB81332E330329004FEDA8 /* NotificationExt.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 77DB81432E33050B004FEDA8 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77DB81412E33050B004FEDA8 /* NotificationService.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + 93D86B8B77018E3DCD97C1C8 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FE600D24FF78AE6CC769C16D /* libPods-ReactNativeSdkExample.a */; }; A3A40C20801B8F02005FA4C0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */; }; - D676F724AF631EA8784FCCAD /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1BF8B855D40C338BCF805B23 /* libPods-ReactNativeSdkExample.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -23,8 +26,29 @@ remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteInfo = ReactNativeSdkExample; }; + 77DB81382E330329004FEDA8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 77DB81322E330329004FEDA8; + remoteInfo = NotificationExt; + }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 77DB813F2E330329004FEDA8 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 77DB813A2E330329004FEDA8 /* NotificationExt.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 00E356EE1AD99517003FC87E /* ReactNativeSdkExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactNativeSdkExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -33,15 +57,22 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ReactNativeSdkExample/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ReactNativeSdkExample/Info.plist; sourceTree = ""; }; 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 1BF8B855D40C338BCF805B23 /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 5ADA50384391946ED83C750E /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; + 65D78EC9700CB673F46D24E3 /* Pods-NotificationExt.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationExt.release.xcconfig"; path = "Target Support Files/Pods-NotificationExt/Pods-NotificationExt.release.xcconfig"; sourceTree = ""; }; + 6F7894ECC91E456C22D0D324 /* Pods-ReactNativeSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.debug.xcconfig"; sourceTree = ""; }; 779227312DFA3FB500D69EC0 /* ReactNativeSdkExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExample-Bridging-Header.h"; sourceTree = ""; }; 779227322DFA3FB500D69EC0 /* ReactNativeSdkExampleTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExampleTests-Bridging-Header.h"; sourceTree = ""; }; 779227332DFA3FB500D69EC0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = ReactNativeSdkExample/AppDelegate.swift; sourceTree = ""; }; + 77DB812E2E3302B4004FEDA8 /* ReactNativeSdkExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = ReactNativeSdkExample.entitlements; path = ReactNativeSdkExample/ReactNativeSdkExample.entitlements; sourceTree = ""; }; + 77DB81332E330329004FEDA8 /* NotificationExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationExt.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 77DB81402E33050B004FEDA8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 77DB81412E33050B004FEDA8 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReactNativeSdkExample/LaunchScreen.storyboard; sourceTree = ""; }; - 90DCEC4520E54C6A13BB5FDC /* Pods-ReactNativeSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.debug.xcconfig"; sourceTree = ""; }; + A9DBF092882E5C0FB40D71CC /* Pods-NotificationExt.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationExt.debug.xcconfig"; path = "Target Support Files/Pods-NotificationExt/Pods-NotificationExt.debug.xcconfig"; sourceTree = ""; }; + A9DDB244D6B41D1A8EE903ED /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + FA966B65909B06BFEB95FCC9 /* libPods-NotificationExt.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NotificationExt.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + FE600D24FF78AE6CC769C16D /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -56,7 +87,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D676F724AF631EA8784FCCAD /* libPods-ReactNativeSdkExample.a in Frameworks */, + 93D86B8B77018E3DCD97C1C8 /* libPods-ReactNativeSdkExample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 77DB81302E330329004FEDA8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 65AD4F06C28F65CB4E898F9A /* libPods-NotificationExt.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -83,6 +122,7 @@ 13B07FAE1A68108700A75B9A /* ReactNativeSdkExample */ = { isa = PBXGroup; children = ( + 77DB812E2E3302B4004FEDA8 /* ReactNativeSdkExample.entitlements */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 779227332DFA3FB500D69EC0 /* AppDelegate.swift */, 13B07FB61A68108700A75B9A /* Info.plist */, @@ -99,11 +139,21 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 1BF8B855D40C338BCF805B23 /* libPods-ReactNativeSdkExample.a */, + FE600D24FF78AE6CC769C16D /* libPods-ReactNativeSdkExample.a */, + FA966B65909B06BFEB95FCC9 /* libPods-NotificationExt.a */, ); name = Frameworks; sourceTree = ""; }; + 77DB81422E33050B004FEDA8 /* NotificationExt */ = { + isa = PBXGroup; + children = ( + 77DB81402E33050B004FEDA8 /* Info.plist */, + 77DB81412E33050B004FEDA8 /* NotificationService.swift */, + ); + path = NotificationExt; + sourceTree = ""; + }; 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( @@ -117,6 +167,7 @@ 13B07FAE1A68108700A75B9A /* ReactNativeSdkExample */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 00E356EF1AD99517003FC87E /* ReactNativeSdkExampleTests */, + 77DB81422E33050B004FEDA8 /* NotificationExt */, 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, BBD78D7AC51CEA395F1C20DB /* Pods */, @@ -131,6 +182,7 @@ children = ( 13B07F961A680F5B00A75B9A /* ReactNativeSdkExample.app */, 00E356EE1AD99517003FC87E /* ReactNativeSdkExampleTests.xctest */, + 77DB81332E330329004FEDA8 /* NotificationExt.appex */, ); name = Products; sourceTree = ""; @@ -138,8 +190,10 @@ BBD78D7AC51CEA395F1C20DB /* Pods */ = { isa = PBXGroup; children = ( - 90DCEC4520E54C6A13BB5FDC /* Pods-ReactNativeSdkExample.debug.xcconfig */, - 5ADA50384391946ED83C750E /* Pods-ReactNativeSdkExample.release.xcconfig */, + 6F7894ECC91E456C22D0D324 /* Pods-ReactNativeSdkExample.debug.xcconfig */, + A9DDB244D6B41D1A8EE903ED /* Pods-ReactNativeSdkExample.release.xcconfig */, + A9DBF092882E5C0FB40D71CC /* Pods-NotificationExt.debug.xcconfig */, + 65D78EC9700CB673F46D24E3 /* Pods-NotificationExt.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -169,29 +223,50 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ReactNativeSdkExample" */; buildPhases = ( - 981F53D791F0E4FE8BCDF7DB /* [CP] Check Pods Manifest.lock */, + A613F9753095F1486E9898F7 /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - CA49B9E1CC35BBC17B422B31 /* [CP] Embed Pods Frameworks */, - CEDF0C37F91A1E965073EBEC /* [CP] Copy Pods Resources */, + 604F8A3ACFF9FF61E3A803F1 /* [CP] Embed Pods Frameworks */, + 2E4E0E4099ECD57D65389F08 /* [CP] Copy Pods Resources */, + 77DB813F2E330329004FEDA8 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + 77DB81392E330329004FEDA8 /* PBXTargetDependency */, ); name = ReactNativeSdkExample; productName = ReactNativeSdkExample; productReference = 13B07F961A680F5B00A75B9A /* ReactNativeSdkExample.app */; productType = "com.apple.product-type.application"; }; + 77DB81322E330329004FEDA8 /* NotificationExt */ = { + isa = PBXNativeTarget; + buildConfigurationList = 77DB813C2E330329004FEDA8 /* Build configuration list for PBXNativeTarget "NotificationExt" */; + buildPhases = ( + 2E911B0C90954AA53C83E406 /* [CP] Check Pods Manifest.lock */, + 77DB812F2E330329004FEDA8 /* Sources */, + 77DB81302E330329004FEDA8 /* Frameworks */, + 77DB81312E330329004FEDA8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NotificationExt; + productName = NotificationExt; + productReference = 77DB81332E330329004FEDA8 /* NotificationExt.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1640; LastUpgradeCheck = 1210; TargetAttributes = { 00E356ED1AD99517003FC87E = { @@ -202,6 +277,9 @@ 13B07F861A680F5B00A75B9A = { LastSwiftMigration = 1640; }; + 77DB81322E330329004FEDA8 = { + CreatedOnToolsVersion = 16.4; + }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "ReactNativeSdkExample" */; @@ -219,6 +297,7 @@ targets = ( 13B07F861A680F5B00A75B9A /* ReactNativeSdkExample */, 00E356ED1AD99517003FC87E /* ReactNativeSdkExampleTests */, + 77DB81322E330329004FEDA8 /* NotificationExt */, ); }; /* End PBXProject section */ @@ -241,6 +320,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 77DB81312E330329004FEDA8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -260,7 +346,24 @@ shellPath = /bin/sh; shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; }; - 981F53D791F0E4FE8BCDF7DB /* [CP] Check Pods Manifest.lock */ = { + 2E4E0E4099ECD57D65389F08 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 2E911B0C90954AA53C83E406 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -275,14 +378,14 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ReactNativeSdkExample-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-NotificationExt-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - CA49B9E1CC35BBC17B422B31 /* [CP] Embed Pods Frameworks */ = { + 604F8A3ACFF9FF61E3A803F1 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -299,21 +402,26 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - CEDF0C37F91A1E965073EBEC /* [CP] Copy Pods Resources */ = { + A613F9753095F1486E9898F7 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Copy Pods Resources"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ReactNativeSdkExample-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -335,6 +443,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 77DB812F2E330329004FEDA8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 77DB81432E33050B004FEDA8 /* NotificationService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -343,6 +459,11 @@ target = 13B07F861A680F5B00A75B9A /* ReactNativeSdkExample */; targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; }; + 77DB81392E330329004FEDA8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 77DB81322E330329004FEDA8 /* NotificationExt */; + targetProxy = 77DB81382E330329004FEDA8 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -406,10 +527,11 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 90DCEC4520E54C6A13BB5FDC /* Pods-ReactNativeSdkExample.debug.xcconfig */; + baseConfigurationReference = 6F7894ECC91E456C22D0D324 /* Pods-ReactNativeSdkExample.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = ReactNativeSdkExample/ReactNativeSdkExample.entitlements; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = BP98Z28R86; ENABLE_BITCODE = NO; @@ -425,7 +547,7 @@ "-ObjC", "-lc++", ); - PRODUCT_BUNDLE_IDENTIFIER = iterable.reactnativesdk.example; + PRODUCT_BUNDLE_IDENTIFIER = com.iterable.reactnativesdk.example; PRODUCT_NAME = ReactNativeSdkExample; SWIFT_OBJC_BRIDGING_HEADER = "ReactNativeSdkExample-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -436,10 +558,11 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5ADA50384391946ED83C750E /* Pods-ReactNativeSdkExample.release.xcconfig */; + baseConfigurationReference = A9DDB244D6B41D1A8EE903ED /* Pods-ReactNativeSdkExample.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = ReactNativeSdkExample/ReactNativeSdkExample.entitlements; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = BP98Z28R86; INFOPLIST_FILE = ReactNativeSdkExample/Info.plist; @@ -454,7 +577,7 @@ "-ObjC", "-lc++", ); - PRODUCT_BUNDLE_IDENTIFIER = iterable.reactnativesdk.example; + PRODUCT_BUNDLE_IDENTIFIER = com.iterable.reactnativesdk.example; PRODUCT_NAME = ReactNativeSdkExample; SWIFT_OBJC_BRIDGING_HEADER = "ReactNativeSdkExample-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -462,6 +585,89 @@ }; name = Release; }; + 77DB813D2E330329004FEDA8 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A9DBF092882E5C0FB40D71CC /* Pods-NotificationExt.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = BP98Z28R86; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationExt/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NotificationExt; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.iterable.reactnativesdk.example.NotificationExt; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 77DB813E2E330329004FEDA8 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 65D78EC9700CB673F46D24E3 /* Pods-NotificationExt.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = BP98Z28R86; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationExt/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NotificationExt; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.iterable.reactnativesdk.example.NotificationExt; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 83CBBA201A601CBA00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -643,6 +849,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 77DB813C2E330329004FEDA8 /* Build configuration list for PBXNativeTarget "NotificationExt" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 77DB813D2E330329004FEDA8 /* Debug */, + 77DB813E2E330329004FEDA8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "ReactNativeSdkExample" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/example/ios/ReactNativeSdkExample/AppDelegate.swift b/example/ios/ReactNativeSdkExample/AppDelegate.swift index 1f3fbca7c..fafc0c46a 100644 --- a/example/ios/ReactNativeSdkExample/AppDelegate.swift +++ b/example/ios/ReactNativeSdkExample/AppDelegate.swift @@ -5,10 +5,12 @@ // Created by Loren Posen on 6/11/25. // -import UIKit +import IterableSDK import React -import React_RCTAppDelegate import ReactAppDependencyProvider +import React_RCTAppDelegate +import UIKit +import UserNotifications @main class AppDelegate: UIResponder, UIApplicationDelegate { @@ -17,7 +19,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var reactNativeDelegate: ReactNativeDelegate? var reactNativeFactory: RCTReactNativeFactory? - func application( + public func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { @@ -36,8 +38,96 @@ class AppDelegate: UIResponder, UIApplicationDelegate { launchOptions: launchOptions ) + UNUserNotificationCenter.current().delegate = self + + /** + * Request permissions for push notifications. + * @see Step 3.5.5 of https://support.iterable.com/hc/en-us/articles/360045714132-Installing-Iterable-s-React-Native-SDK#step-3-5-set-up-support-for-push-notifications + */ + requestPushPermissions(application) + return true } + + /** + * Add support for in-app messages + * @see Step 3.6 of https://support.iterable.com/hc/en-us/articles/360045714132-Installing-Iterable-s-React-Native-SDK#step-3-6-add-support-for-in-app-messages + */ + public func application( + _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + IterableAppIntegration.application( + application, didReceiveRemoteNotification: userInfo, + fetchCompletionHandler: completionHandler + ) + NSLog("didReceiveRemoteNotification: \(userInfo)") + } + + public func application( + _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { + /** + * Register the device token with Iterable. + * @see Step 3.5.4 of https://support.iterable.com/hc/en-us/articles/360045714132-Installing-Iterable-s-React-Native-SDK#step-3-5-set-up-support-for-push-notifications + */ + IterableAPI.register(token: deviceToken) + NSLog("didRegisterForRemoteNotificationsWithDeviceToken: \(deviceToken)") + } + + /** + * Add support for deep links + * @see Step 3.7 of https://support.iterable.com/hc/en-us/articles/360045714132-Installing-Iterable-s-React-Native-SDK#step-3-7-add-support-for-deep-links + */ + public func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + return RCTLinkingManager.application( + application, + continue: userActivity, + restorationHandler: restorationHandler + ) + } + + /** + * Add support for deep links + * @see Step 3.7 of https://support.iterable.com/hc/en-us/articles/360045714132-Installing-Iterable-s-React-Native-SDK#step-3-7-add-support-for-deep-links + */ + public func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + return RCTLinkingManager.application(app, open: url, options: options) + } + + public func requestPushPermissions(_ application: UIApplication) { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { + granted, _ in + DispatchQueue.main.async { + if granted { + application.registerForRemoteNotifications() + } else { + NSLog("Push permission denied") + } + } + } + // UNUserNotificationCenter.current().getNotificationSettings { (settings) in + // if settings.authorizationStatus != .authorized { + // NSLog("Not authorized") + // // not authorized, ask for permission + // UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { + // (success, error) in + // NSLog("auth: \(success)") + // } + // } else { + // // already authorized + // NSLog("Already authorized") + // } + // } + } } class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { @@ -46,11 +136,31 @@ class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { } override func bundleURL() -> URL? { -#if DEBUG - RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") -#else - Bundle.main.url(forResource: "main", withExtension: "jsbundle") -#endif + #if DEBUG + RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") + #else + Bundle.main.url(forResource: "main", withExtension: "jsbundle") + #endif } } +/// * Handle incoming push notifications and enable push notification tracking. +/// * @see Step 3.5.5 of https://support.iterable.com/hc/en-us/articles/360045714132-Installing-Iterable-s-React-Native-SDK#step-3-5-set-up-support-for-push-notifications +extension AppDelegate: UNUserNotificationCenterDelegate { + public func userNotificationCenter( + _: UNUserNotificationCenter, willPresent _: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + completionHandler([.badge, .banner, .list, .sound]) + NSLog("willPresent") + } + + public func userNotificationCenter( + _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + IterableAppIntegration.userNotificationCenter( + center, didReceive: response, withCompletionHandler: completionHandler) + NSLog("didReceive") + } +} diff --git a/example/ios/ReactNativeSdkExample/Info.plist b/example/ios/ReactNativeSdkExample/Info.plist index 4e0430cdd..02f744389 100644 --- a/example/ios/ReactNativeSdkExample/Info.plist +++ b/example/ios/ReactNativeSdkExample/Info.plist @@ -22,15 +22,14 @@ ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) - LSRequiresIPhoneOS - LSApplicationQueriesSchemes - javascript + javascript + LSRequiresIPhoneOS + NSAppTransportSecurity - NSAllowsArbitraryLoads NSAllowsLocalNetworking @@ -38,20 +37,6 @@ NSLocationWhenInUseUsageDescription - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - UIAppFonts AntDesign.ttf @@ -74,5 +59,23 @@ Zocial.ttf Fontisto.ttf + UIBackgroundModes + + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + diff --git a/example/ios/ReactNativeSdkExample/ReactNativeSdkExample.entitlements b/example/ios/ReactNativeSdkExample/ReactNativeSdkExample.entitlements new file mode 100644 index 000000000..31c7e997a --- /dev/null +++ b/example/ios/ReactNativeSdkExample/ReactNativeSdkExample.entitlements @@ -0,0 +1,10 @@ + + + + + aps-environment + development + com.apple.developer.usernotifications.time-sensitive + + + diff --git a/example/src/components/App/App.constants.ts b/example/src/components/App/App.constants.ts index f84c390cb..ca1bbdd41 100644 --- a/example/src/components/App/App.constants.ts +++ b/example/src/components/App/App.constants.ts @@ -4,4 +4,5 @@ export const routeIcon = { [Route.Commerce]: 'cash-outline', [Route.Inbox]: 'mail-outline', [Route.User]: 'person-outline', + [Route.Utility]: 'build-outline', }; diff --git a/example/src/components/App/Main.tsx b/example/src/components/App/Main.tsx index 55b0d74e2..31190661d 100644 --- a/example/src/components/App/Main.tsx +++ b/example/src/components/App/Main.tsx @@ -8,6 +8,7 @@ import { User } from '../User'; import { Inbox } from '../Inbox'; import { useIterableApp } from '../../hooks'; import { Commerce } from '../Commerce'; +import { Utility } from '../Utility'; const Tab = createBottomTabNavigator(); @@ -58,6 +59,13 @@ export const Main = () => { tabPress: () => setIsInboxTab(false), })} /> + ({ + tabPress: () => setIsInboxTab(false), + })} + /> ); diff --git a/example/src/components/Utility/Utility.styles.ts b/example/src/components/Utility/Utility.styles.ts new file mode 100644 index 000000000..0ec5c0cbc --- /dev/null +++ b/example/src/components/Utility/Utility.styles.ts @@ -0,0 +1,17 @@ +import { StyleSheet, type TextStyle } from 'react-native'; +import { appNameSmall, buttonBlock, buttonText, container } from '../../constants'; + +const text: TextStyle = { + textAlign: 'center', + marginBottom: 20, +}; + +const styles = StyleSheet.create({ + appName: appNameSmall, + button:buttonBlock, + buttonText, + container, + text, +}); + +export default styles; diff --git a/example/src/components/Utility/Utility.tsx b/example/src/components/Utility/Utility.tsx new file mode 100644 index 000000000..218f8719a --- /dev/null +++ b/example/src/components/Utility/Utility.tsx @@ -0,0 +1,76 @@ +import { Iterable, RNIterableAPI } from '@iterable/react-native-sdk'; +import { useEffect } from 'react'; +import { NativeEventEmitter, Text, TouchableOpacity, View } from 'react-native'; + +import styles from './Utility.styles'; + +const newEmitter = new NativeEventEmitter(RNIterableAPI); + +export const Utility = () => { + useEffect(() => { + console.log(`🚀 > RNIterableAPI:`, RNIterableAPI); + + const newSub = newEmitter.addListener('onTestEventDispatch', (event) => { + console.log('*** ITBL JS *** RECEIVED onTestEventDispatch:', event); + }); + return () => { + newSub.remove(); + }; + }, []); + + return ( + + Utility + { + Iterable.getEmail().then((email) => { + console.log('Iterable.getEmail() --> email', email); + }); + }}> + Iterable.getEmail()aeff + + { + Iterable.getUserId().then((userId) => { + console.log('Iterable.getUserId() --> userId', userId); + }); + }}> + Iterable.getUserId() + + { + Iterable.getAttributionInfo().then((attributionInfo) => { + console.log('Iterable.getAttributionInfo() --> attributionInfo', attributionInfo); + }); + }}> + Iterable.getAttributionInfo() + + { + Iterable.setAttributionInfo({ + campaignId: 123, + templateId: 456, + messageId: '789', + }); + }}> + Iterable.setAttributionInfo() + + { + Iterable.disableDeviceForCurrentUser(); + }}> + Iterable.disableDeviceForCurrentUser() + + { + Iterable.getLastPushPayload().then((lastPushPayload) => { + console.log('Iterable.getLastPushPayload() --> lastPushPayload', lastPushPayload); + }); + }}> + Iterable.getLastPushPayload() + + { + console.log('*** ITBL JS *** SENDING testEventDispatch'); + RNIterableAPI.testEventDispatch(); + }}> + dispatch test event + + + ); +}; + +export default Utility; diff --git a/example/src/components/Utility/index.ts b/example/src/components/Utility/index.ts new file mode 100644 index 000000000..acc470409 --- /dev/null +++ b/example/src/components/Utility/index.ts @@ -0,0 +1,2 @@ +export * from './Utility'; +export { default } from './Utility'; diff --git a/example/src/constants/routes.ts b/example/src/constants/routes.ts index 4af27c548..317ea5a2a 100644 --- a/example/src/constants/routes.ts +++ b/example/src/constants/routes.ts @@ -4,4 +4,5 @@ export enum Route { Login = 'Login', Main = 'Main', User = 'User', + Utility = 'Utility', } diff --git a/example/src/types/navigation.ts b/example/src/types/navigation.ts index 5b5ad8a50..6435adb0d 100644 --- a/example/src/types/navigation.ts +++ b/example/src/types/navigation.ts @@ -11,6 +11,7 @@ export type MainScreenParamList = { [Route.Commerce]: undefined; [Route.Inbox]: undefined; [Route.User]: undefined; + [Route.Utility]: undefined; }; export type RootStackParamList = { diff --git a/ios/RNIterableAPI/RNIterableAPI.h b/ios/RNIterableAPI/RNIterableAPI.h index be3fee829..ee849df3a 100644 --- a/ios/RNIterableAPI/RNIterableAPI.h +++ b/ios/RNIterableAPI/RNIterableAPI.h @@ -1,15 +1,17 @@ #import -#import #if RCT_NEW_ARCH_ENABLED #import -@interface RNIterableAPI : RCTEventEmitter +#import +#import +#import +@interface RNIterableAPI : NSObject -#else - -#import -@interface RNIterableAPI : RCTEventEmitter +// #else +// #import +// #import +// @interface RNIterableAPI : RCTEventEmitter #endif diff --git a/ios/RNIterableAPI/RNIterableAPI.mm b/ios/RNIterableAPI/RNIterableAPI.mm index bef0a765b..eae832ce5 100644 --- a/ios/RNIterableAPI/RNIterableAPI.mm +++ b/ios/RNIterableAPI/RNIterableAPI.mm @@ -23,25 +23,66 @@ @implementation RNIterableAPI { } - (instancetype)init { - self = [super init]; - if(self) { - // Option 2.B - Instantiate the Calculator and set the delegate + if (self = [super init]) { _swiftAPI = [ReactIterableAPI new]; - _swiftAPI.delegate = self; + // [_api configureWithBridge:self.bridge]; } return self; } +// - (instancetype)init { +// self = [super init]; +// if(self) { +// // Option 2.B - Instantiate the Calculator and set the delegate +// _swiftAPI = [ReactIterableAPI shared]; +// _swiftAPI.delegate = self; +// } +// return self; +// } + RCT_EXPORT_MODULE() -- (NSArray *)supportedEvents { - return [ReactIterableAPI supportedEvents]; +// - (NSArray *)supportedEvents { +// return [ReactIterableAPI supportedEvents]; +// } + + +// // 2) When JS calls emitter.addListener(), React will call this on *your* module. +// // Forward it to the Swift emitter so that its `startObserving()` fires. +// - (void)startObserving { +// [_swiftAPI startObserving]; +// } + +// // 3) When JS calls emitter.removeListeners(n), React will call this. +// // Forward it to Swift so its `stopObserving()` fires. +// - (void)stopObserving { +// [_swiftAPI stopObserving]; +// } + +// // 4) Swift will call this delegate method when it wants to emit. +// // Forward it up to React’s `sendEventWithName:body:` +// - (void)sendEventWithName:(NSString *)name body:(id)body { +// [super sendEventWithName:name body:body]; +// } + +- (void)addListener:(NSString *)eventName { + // No-op; required to prevent selector crash } -- (void)sendEventWithName:(NSString * _Nonnull)name result:(double)result { - [self sendEventWithName:name body:@(result)]; +- (void)removeListeners:(double)count { + // No-op; required to prevent selector crash } +// - (void)addListener:(NSString *)eventName +// { +// [_swiftAPI.delegate addListener:eventName]; +// } + +// - (void)removeListeners:(double)count +// { +// [_swiftAPI.delegate removeListeners:count]; +// } + - (void)testEventDispatch { [_swiftAPI testEventDispatch]; } @@ -251,10 +292,10 @@ - (void)handleAppLink:(NSString *)appLink [_swiftAPI handleAppLink:appLink resolver:resolve rejecter:reject]; } -- (void)updateSubscriptions:(NSArray *)emailListIds - unsubscribedChannelIds:(NSArray *)unsubscribedChannelIds - unsubscribedMessageTypeIds:(NSArray *)unsubscribedMessageTypeIds - subscribedMessageTypeIds:(NSArray *)subscribedMessageTypeIds +- (void)updateSubscriptions:(NSArray * _Nullable)emailListIds + unsubscribedChannelIds:(NSArray * _Nullable)unsubscribedChannelIds + unsubscribedMessageTypeIds:(NSArray * _Nullable)unsubscribedMessageTypeIds + subscribedMessageTypeIds:(NSArray * _Nullable)subscribedMessageTypeIds campaignId:(NSNumber *)campaignId templateId:(NSNumber *)templateId { @@ -281,6 +322,10 @@ - (void)passAlongAuthToken:(NSString *)authToken [_swiftAPI passAlongAuthToken:authToken]; } +using namespace facebook::react; + + + - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index f70c8b997..0c3fd3efb 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -1,32 +1,90 @@ import Foundation import IterableSDK import React + @objc public protocol ReactIterableAPIDelegate { func sendEvent(withName: String, body: Any?) } -@objc(ReactIterableAPI) -public class ReactIterableAPI: RCTEventEmitter { - deinit { - NotificationCenter.default.removeObserver(self) +@objc protocol RCTAppContext { + func emit(eventName: String, payload: Any?) +} + +@objc(BridgelessEventEmitterHost) +class BridgelessEventEmitterHost: NSObject { + private let appContext: RCTAppContext + + init(appContext: RCTAppContext) { + self.appContext = appContext + super.init() + NSLog("*** BridgelessEventEmitterHost initialized ***") } - @objc public weak var delegate: ReactIterableAPIDelegate? = nil + func send(name: String, body: Any?) { + appContext.emit(eventName: name, payload: body) + } - // MARK: - React Native Functions + func getShouldEmit() -> Bool { + // Optional logic if you want to control flow + return true + } +} + +class EventDispatcher { + private var bridgelessEmitter: BridgelessEventEmitterHost? + + init(appContext: RCTAppContext) { + NSLog("*** BRIDGELESS EventDispatcher init ***") + self.bridgelessEmitter = BridgelessEventEmitterHost(appContext: appContext) + } + + func send(name: String, body: Any?) { + if let emitter = bridgelessEmitter { + emitter.send(name: name, body: body) + } else { + NSLog("⚠️ No emitter available to send event: \(name)") + } + } - @objc override public class func moduleName() -> String! { - return "RNIterableAPI" + func getShouldEmit() -> Bool { + return bridgelessEmitter?.getShouldEmit() ?? false } +} + +@objc(ReactIterableAPI) +public class ReactIterableAPI: NSObject { + @objc public static let shared = ReactIterableAPI() + + private var savedLaunchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + private var eventDispatcher: EventDispatcher? - override open var methodQueue: DispatchQueue! { - _methodQueue + override public init() { + super.init() + + NSLog("*** ReactIterableAPI init ***") } - @objc override public static func requiresMainQueueSetup() -> Bool { - false + init(appContext: RCTAppContext) { + eventDispatcher = EventDispatcher(appContext: appContext) } +// @objc public weak var delegate: ReactIterableAPIDelegate? = nil + + // MARK: - React Native Functions + +// @objc override public class func moduleName() -> String! { +// return "RNIterableAPI" +// } + + // IMPORTANT: This is part of legacy event emitter +// override open var methodQueue: DispatchQueue! { +// _methodQueue +// } + +// @objc override public static func requiresMainQueueSetup() -> Bool { +// false +// } + enum EventName: String, CaseIterable { case handleUrlCalled case handleCustomActionCalled @@ -38,25 +96,9 @@ public class ReactIterableAPI: RCTEventEmitter { case onTestEventDispatch } - @objc public static var supportedEvents: [String] { - return EventName.allCases.map(\.rawValue) - } - - override public func startObserving() { - ITBInfo() - - shouldEmit = true - } - - override public func stopObserving() { - ITBInfo() - - shouldEmit = false - } - @objc(testEventDispatch) public func testEventDispatch() { - delegate?.sendEvent(withName: EventName.onTestEventDispatch.rawValue, body: 0) + eventDispatcher?.send(name: EventName.onTestEventDispatch.rawValue, body: 0) } // MARK: - Native SDK Functions @@ -510,6 +552,7 @@ public class ReactIterableAPI: RCTEventEmitter { ITBInfo() let launchOptions = createLaunchOptions() + NSLog("*** launchOptions: \(launchOptions) ***") let iterableConfig = IterableConfig.from( dict: configDict as? [AnyHashable: Any] ) @@ -547,22 +590,39 @@ public class ReactIterableAPI: RCTEventEmitter { @objc(receivedIterableInboxChanged) public func receivedIterableInboxChanged() { - guard shouldEmit else { - return - } + NSLog("*** receivedIterableInboxChanged1 ***") +// guard shouldEmit else { +// return +// } + + NSLog("***SEND EVENT***: receivedIterableInboxChanged") - delegate?.sendEvent(withName: EventName.receivedIterableInboxChanged.rawValue, body: nil) + eventDispatcher?.send(name: EventName.receivedIterableInboxChanged.rawValue, body: nil) +// sendEvent(withName: EventName.receivedIterableInboxChanged.rawValue, body: nil) } private func createLaunchOptions() -> [UIApplication.LaunchOptionsKey: Any]? { - guard let bridge = self.bridge else { - return nil - } + return nil + // new‐arch path: use what we captured +// if let opts = savedLaunchOptions { +// return ReactIterableAPI.createLaunchOptions(bridgeLaunchOptions: opts) +// } + + // legacy‐bridge path left in if you’re still building for RCTBridge +// if let legacy = bridge?.launchOptions { +// return ReactIterableAPI.createLaunchOptions(bridgeLaunchOptions: legacy) +// } - return ReactIterableAPI.createLaunchOptions(bridgeLaunchOptions: bridge.launchOptions) +// return nil + // guard let bridge = self.bridge else { + // return nil + // } + + // return ReactIterableAPI.createLaunchOptions(bridgeLaunchOptions: bridge.launchOptions) } private static func createLaunchOptions(bridgeLaunchOptions: [AnyHashable: Any]?) -> [UIApplication.LaunchOptionsKey: Any]? { + NSLog("*** createLaunchOptions2 ***: \(bridgeLaunchOptions)") guard let bridgeLaunchOptions = bridgeLaunchOptions, let remoteNotification = bridgeLaunchOptions[UIApplication.LaunchOptionsKey.remoteNotification.rawValue] else { return nil @@ -579,12 +639,15 @@ extension ReactIterableAPI: IterableURLDelegate { public func handle(iterableURL url: URL, inContext context: IterableActionContext) -> Bool { ITBInfo() - guard shouldEmit else { - return false - } +// guard shouldEmit else { +// return false +// } let contextDict = ReactIterableAPI.contextToDictionary(context: context) - delegate?.sendEvent(withName: EventName.handleUrlCalled.rawValue, + + NSLog("***SEND EVENT***: handle(iterableURL url: \(url.absoluteString), inContext context: \(contextDict)") + + eventDispatcher?.send(name: EventName.handleUrlCalled.rawValue, body: ["url": url.absoluteString, "context": contextDict] as [String : Any]) @@ -625,7 +688,9 @@ extension ReactIterableAPI: IterableCustomActionDelegate { let actionDict = ReactIterableAPI.actionToDictionary(action: action) let contextDict = ReactIterableAPI.contextToDictionary(context: context) - delegate?.sendEvent(withName: EventName.handleCustomActionCalled.rawValue, + NSLog("***SEND EVENT***: handle(iterableCustomAction action: \(actionDict), inContext context: \(contextDict)") + + eventDispatcher?.send(name: EventName.handleCustomActionCalled.rawValue, body: ["action": actionDict, "context": contextDict]) @@ -637,11 +702,13 @@ extension ReactIterableAPI: IterableInAppDelegate { public func onNew(message: IterableInAppMessage) -> InAppShowResponse { ITBInfo() - guard shouldEmit else { - return .show - } +// guard shouldEmit else { +// return .show +// } + + NSLog("***SEND EVENT***: onNew(message: \(message.toDict())") - delegate?.sendEvent(withName: EventName.handleInAppCalled.rawValue, + eventDispatcher?.send(name: EventName.handleInAppCalled.rawValue, body: message.toDict()) let timeoutResult = inAppHandlerSemaphore.wait(timeout: .now() + 2.0) @@ -661,7 +728,9 @@ extension ReactIterableAPI: IterableAuthDelegate { ITBInfo() DispatchQueue.global(qos: .userInitiated).async { - self.sendEvent(withName: EventName.handleAuthCalled.rawValue, + NSLog("***SEND EVENT***: onAuthTokenRequested") + + self.eventDispatcher?.send(name: EventName.handleAuthCalled.rawValue, body: nil as Any?) let authTokenRetrievalResult = self.authHandlerSemaphore.wait(timeout: .now() + 30.0) @@ -673,7 +742,10 @@ extension ReactIterableAPI: IterableAuthDelegate { completion(self.passedAuthToken) } - self.sendEvent(withName: EventName.handleAuthSuccessCalled.rawValue, + NSLog("***SEND EVENT***: handleAuthSuccessCalled") + + self.eventDispatcher? + .send(name: EventName.handleAuthSuccessCalled.rawValue, body: nil as Any?) } else { ITBInfo("authTokenRetrieval timed out") @@ -682,7 +754,10 @@ extension ReactIterableAPI: IterableAuthDelegate { completion(nil) } - self.sendEvent(withName: EventName.handleAuthFailureCalled.rawValue, + NSLog("***SEND EVENT***: handleAuthFailureCalled") + + self.eventDispatcher? + .send(name: EventName.handleAuthFailureCalled.rawValue, body: nil as Any?) } } diff --git a/src/inbox/components/IterableInbox.tsx b/src/inbox/components/IterableInbox.tsx index 7e33b5969..a9529af7b 100644 --- a/src/inbox/components/IterableInbox.tsx +++ b/src/inbox/components/IterableInbox.tsx @@ -325,6 +325,7 @@ export const IterableInbox = ({ function addInboxChangedListener() { RNEventEmitter.addListener('receivedIterableInboxChanged', () => { + console.log('*** ITBL JS *** receivedIterableInboxChanged'); fetchInboxMessages(); }); } diff --git a/src/index.tsx b/src/index.tsx index 885cd74bd..8dad4859a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,7 @@ /** * React Native module for Iterable. */ +export { RNIterableAPI } from './api'; export { Iterable, IterableAction, From 6b481d927061ba4e30e8a0700e4594f3217e7f18 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Sat, 26 Jul 2025 14:44:31 -0700 Subject: [PATCH 6/8] feat: working event emitter --- BridgelessEventEmitterHost.swift | 22 + EventDispatcher.swift | 28 - EventEmitterHostLegacy.h | 3 - EventEmitterHostLegacy.mm | 4 - EventEmitterHostLegacy.swift | 48 - EventEmitterHostNew.swift | 50 - example/src/hooks/useIterableApp.tsx | 12 + ios/RNIterableAPI/RNIterableAPI.h | 5 +- ios/RNIterableAPI/RNIterableAPI.mm | 112 +- ios/RNIterableAPI/ReactIterableAPI.swift | 1421 ++++++++++------------ package.json | 3 + src/api/index.ts | 12 +- src/global.d.ts | 3 - yarn.lock | 3 + 14 files changed, 782 insertions(+), 944 deletions(-) create mode 100644 BridgelessEventEmitterHost.swift delete mode 100644 EventDispatcher.swift delete mode 100644 EventEmitterHostLegacy.h delete mode 100644 EventEmitterHostLegacy.mm delete mode 100644 EventEmitterHostLegacy.swift delete mode 100644 EventEmitterHostNew.swift delete mode 100644 src/global.d.ts diff --git a/BridgelessEventEmitterHost.swift b/BridgelessEventEmitterHost.swift new file mode 100644 index 000000000..f3b587646 --- /dev/null +++ b/BridgelessEventEmitterHost.swift @@ -0,0 +1,22 @@ +import Foundation +import React + +@objc(BridgelessEventEmitterHost) +class BridgelessEventEmitterHost: NSObject { + private let appContext: RCTAppContext + + init(appContext: RCTAppContext) { + self.appContext = appContext + super.init() + NSLog("*** BridgelessEventEmitterHost initialized ***") + } + + func send(name: String, body: Any?) { + appContext.emit(eventName: name, payload: body) + } + + func getShouldEmit() -> Bool { + // Optional logic if you want to control flow + return true + } +} diff --git a/EventDispatcher.swift b/EventDispatcher.swift deleted file mode 100644 index fc59e204d..000000000 --- a/EventDispatcher.swift +++ /dev/null @@ -1,28 +0,0 @@ -// class EventDispatcherAnother { -// private var legacyEmitter: EventEmitterHostLegacy? -// private var bridgelessEmitter: EventEmitterHostNew? - -// init(bridge: RCTBridge) { -// NSLog("*** LEGACY EventDispatcher init ***") -// self.legacyEmitter = bridge.module(for: EventEmitterHostLegacy.self) as? EventEmitterHostLegacy -// } - -// init(appContext: RCTAppContext) { -// NSLog("*** BRIDGELESS EventDispatcher init ***") -// self.bridgelessEmitter = EventEmitterHostNew(appContext: appContext) -// } - -// func send(name: String, body: Any?) { -// if let emitter = legacyEmitter { -// emitter.send(name: name, body: body) -// } else if let emitter = bridgelessEmitter { -// emitter.send(name: name, body: body) -// } else { -// NSLog("⚠️ No emitter available to send event: \(name)") -// } -// } - -// func getShouldEmit() -> Bool { -// return legacyEmitter?.getShouldEmit() ?? bridgelessEmitter?.getShouldEmit() ?? false -// } -// } diff --git a/EventEmitterHostLegacy.h b/EventEmitterHostLegacy.h deleted file mode 100644 index 4b826ed1d..000000000 --- a/EventEmitterHostLegacy.h +++ /dev/null @@ -1,3 +0,0 @@ -// #import -// @interface EventEmitterHostLegacy : RCTEventEmitter -// @end diff --git a/EventEmitterHostLegacy.mm b/EventEmitterHostLegacy.mm deleted file mode 100644 index 6675f64a9..000000000 --- a/EventEmitterHostLegacy.mm +++ /dev/null @@ -1,4 +0,0 @@ -// #import "EventEmitterHostLegacy.h" -// @implementation EventEmitterHostLegacy -// RCT_EXPORT_MODULE() -// @end diff --git a/EventEmitterHostLegacy.swift b/EventEmitterHostLegacy.swift deleted file mode 100644 index 0f05cf20c..000000000 --- a/EventEmitterHostLegacy.swift +++ /dev/null @@ -1,48 +0,0 @@ -// // EventEmitterHost.swift -// import Foundation -// import React - -// @objc class EventEmitterHostLegacy: RCTEventEmitter { -// static let shared = EventEmitterHost() - -// private var shouldEmit = false - -// override init() { -// super.init() -// NSLog("*** EventEmitterHost initialized ***") -// } - -// override class func moduleName() -> String! { -// return "EventEmitterHost" -// } - -// override func supportedEvents() -> [String] { -// return ReactIterableAPI.EventName.allCases.map(\.rawValue) -// } - -// override class func requiresMainQueueSetup() -> Bool { -// return false -// } - -// override func startObserving() { -// ITBInfo() -// shouldEmit = true -// } - -// override func stopObserving() { -// ITBInfo() -// shouldEmit = false -// } - -// func send(name: String, body: Any?) { -// guard shouldEmit else { -// NSLog("[EventEmitterHost] Skipping emit: \(name), no listeners") -// return -// } -// sendEvent(withName: name, body: body) -// } - -// func getShouldEmit() -> Bool { -// return shouldEmit -// } -// } diff --git a/EventEmitterHostNew.swift b/EventEmitterHostNew.swift deleted file mode 100644 index 283c870ff..000000000 --- a/EventEmitterHostNew.swift +++ /dev/null @@ -1,50 +0,0 @@ -// import Foundation -// import React - -// @objc(EventEmitterHostNew) -// class EventEmitterHostNew: NSObject { -// private let appContext: RCTAppContext - -// init(appContext: RCTAppContext) { -// self.appContext = appContext -// super.init() -// NSLog("*** EventEmitterHostNew initialized ***") -// } - -// func send(name: String, body: Any?) { -// appContext.emit(eventName: name, payload: body) -// } - -// func getShouldEmit() -> Bool { -// // Optional logic if you want to control flow -// return true -// } -// } - - - - -// // class EventEmitterHostNew { -// // private var legacyEmitter: LegacyEventEmitter? -// // private var bridgelessEmitter: BridgelessEventEmitter? - -// // init(bridge: RCTBridge) { -// // NSLog("*** LEGACY EventDispatcher init ***") -// // self.legacyEmitter = bridge.module(for: LegacyEventEmitter.self) as? LegacyEventEmitter -// // } - -// // init(appContext: RCTAppContext) { -// // NSLog("*** BRIDGELESS EventDispatcher init ***") -// // self.bridgelessEmitter = BridgelessEventEmitter(appContext: appContext) -// // } - -// // func send(name: String, body: Any?) { -// // if let emitter = legacyEmitter { -// // emitter.send(name, body: body) -// // } else if let emitter = bridgelessEmitter { -// // emitter.send(name, body: body) -// // } else { -// // NSLog("⚠️ No event emitter available to send event: \(name)") -// // } -// // } -// // } diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index ca115a48c..682db228f 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -4,6 +4,7 @@ import { createContext, useCallback, useContext, + useEffect, useState, } from 'react'; import { Alert } from 'react-native'; @@ -14,10 +15,14 @@ import { IterableConfig, IterableInAppShowResponse, IterableLogLevel, + RNIterableAPI, } from '@iterable/react-native-sdk'; import { Route } from '../constants/routes'; import type { RootStackParamList } from '../types/navigation'; +import { NativeEventEmitter } from 'react-native'; + +const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); type Navigation = StackNavigationProp; @@ -99,6 +104,13 @@ export const IterableAppProvider: FunctionComponent< const [userId, setUserId] = useState(process.env.ITBL_ID); const [loginInProgress, setLoginInProgress] = useState(false); + useEffect(() => { + console.log('*** EXAMPLE JS SETTING EVENT LISTENER *** : receivedIterableInboxChanged', RNEventEmitter); + RNEventEmitter.addListener('receivedIterableInboxChanged', (event) => { + console.log('*** EXAMPLE JS EVENT RECEIVED *** : receivedIterableInboxChanged', event); + }); + }, []); + const getUserId = useCallback(() => userId ?? process.env.ITBL_ID, [userId]); const login = useCallback(() => { diff --git a/ios/RNIterableAPI/RNIterableAPI.h b/ios/RNIterableAPI/RNIterableAPI.h index ee849df3a..71380823d 100644 --- a/ios/RNIterableAPI/RNIterableAPI.h +++ b/ios/RNIterableAPI/RNIterableAPI.h @@ -1,12 +1,13 @@ #import +#import #if RCT_NEW_ARCH_ENABLED -#import #import #import #import -@interface RNIterableAPI : NSObject +#import +@interface RNIterableAPI : RCTEventEmitter // #else // #import diff --git a/ios/RNIterableAPI/RNIterableAPI.mm b/ios/RNIterableAPI/RNIterableAPI.mm index eae832ce5..de1ac5346 100644 --- a/ios/RNIterableAPI/RNIterableAPI.mm +++ b/ios/RNIterableAPI/RNIterableAPI.mm @@ -23,67 +23,46 @@ @implementation RNIterableAPI { } - (instancetype)init { - if (self = [super init]) { + self = [super init]; + if(self) { + // Option 2.B - Instantiate the Calculator and set the delegate _swiftAPI = [ReactIterableAPI new]; - // [_api configureWithBridge:self.bridge]; + _swiftAPI.delegate = self; } return self; } -// - (instancetype)init { -// self = [super init]; -// if(self) { -// // Option 2.B - Instantiate the Calculator and set the delegate -// _swiftAPI = [ReactIterableAPI shared]; -// _swiftAPI.delegate = self; -// } -// return self; -// } - RCT_EXPORT_MODULE() // - (NSArray *)supportedEvents { -// return [ReactIterableAPI supportedEvents]; -// } - - -// // 2) When JS calls emitter.addListener(), React will call this on *your* module. -// // Forward it to the Swift emitter so that its `startObserving()` fires. -// - (void)startObserving { -// [_swiftAPI startObserving]; -// } - -// // 3) When JS calls emitter.removeListeners(n), React will call this. -// // Forward it to Swift so its `stopObserving()` fires. -// - (void)stopObserving { -// [_swiftAPI stopObserving]; +// return [_swiftAPI supportedEvents]; // } -// // 4) Swift will call this delegate method when it wants to emit. -// // Forward it up to React’s `sendEventWithName:body:` -// - (void)sendEventWithName:(NSString *)name body:(id)body { -// [super sendEventWithName:name body:body]; -// } +- (NSArray *)supportedEvents { + return [ReactIterableAPI supportedEvents]; +} -- (void)addListener:(NSString *)eventName { - // No-op; required to prevent selector crash +- (void)startObserving { + NSLog(@"ReactNativeSdk startObserving"); + [(ReactIterableAPI *)_swiftAPI startObserving]; } -- (void)removeListeners:(double)count { - // No-op; required to prevent selector crash +- (void)stopObserving { + NSLog(@"ReactNativeSdk stopObserving"); + [(ReactIterableAPI *)_swiftAPI stopObserving]; } -// - (void)addListener:(NSString *)eventName -// { -// [_swiftAPI.delegate addListener:eventName]; -// } +- (void)hello { + NSLog(@"Hello from Objective-C"); + [(ReactIterableAPI *)_swiftAPI hello]; +} -// - (void)removeListeners:(double)count -// { -// [_swiftAPI.delegate removeListeners:count]; -// } +- (void)sendEventWithName:(NSString * _Nonnull)name result:(double)result { + [self sendEventWithName:name body:@(result)]; +} - (void)testEventDispatch { + NSLog(@"***ITBL OBJ-C*** testEventDispatch"); [_swiftAPI testEventDispatch]; } @@ -93,6 +72,7 @@ - (void)initializeWithApiKey:(NSString *)apiKey resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + NSLog(@"ReactNativeSdk initializeWithApiKey"); [_swiftAPI initializeWithApiKey:apiKey config:config version:version @@ -107,6 +87,7 @@ - (void)initialize2WithApiKey:(NSString *)apiKey resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + NSLog(@"ReactNativeSdk initialize2WithApiKey"); [_swiftAPI initialize2WithApiKey:apiKey config:config apiEndPointOverride:apiEndPointOverride @@ -118,41 +99,48 @@ - (void)initialize2WithApiKey:(NSString *)apiKey - (void)setEmail:(NSString * _Nullable)email authToken:(NSString * _Nullable)authToken { + NSLog(@"ReactNativeSdk setEmail"); [_swiftAPI setEmail:email authToken:authToken]; } - (void)getEmail:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + NSLog(@"ReactNativeSdk getEmail"); [_swiftAPI getEmail:resolve rejecter:reject]; } - (void)setUserId:(NSString * _Nullable)userId authToken:(NSString * _Nullable)authToken { + NSLog(@"ReactNativeSdk setUserId"); [_swiftAPI setUserId:userId authToken:authToken]; } - (void)getUserId:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + NSLog(@"ReactNativeSdk getUserId"); [_swiftAPI getUserId:resolve rejecter:reject]; } - (void)setInAppShowResponse:(NSNumber *)inAppShowResponse { + NSLog(@"ReactNativeSdk setInAppShowResponse"); [_swiftAPI setInAppShowResponse:inAppShowResponse]; } - (void)getInAppMessages:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + NSLog(@"ReactNativeSdk getInAppMessages"); [_swiftAPI getInAppMessages:resolve rejecter:reject]; } - (void)getInboxMessages:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + NSLog(@"ReactNativeSdk getInboxMessages"); [_swiftAPI getInboxMessages:resolve rejecter:reject]; } @@ -160,6 +148,7 @@ - (void)getInboxMessages:(RCTPromiseResolveBlock)resolve - (void)getUnreadInboxMessagesCount:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + NSLog(@"ReactNativeSdk getUnreadInboxMessagesCount"); [_swiftAPI getUnreadInboxMessagesCount:resolve rejecter:reject]; } @@ -168,6 +157,7 @@ - (void)showMessage:(NSString *)messageId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + NSLog(@"ReactNativeSdk showMessage"); [_swiftAPI showMessage:messageId consume:consume resolver:resolve rejecter:reject]; } @@ -175,23 +165,27 @@ - (void)removeMessage:(NSString *)messageId location:(NSNumber *)location source:(NSNumber *)source { + NSLog(@"ReactNativeSdk removeMessage"); [_swiftAPI removeMessage:messageId location:location source:source]; } - (void)setReadForMessage:(NSString *)messageId read:(BOOL)read { + NSLog(@"ReactNativeSdk setReadForMessage"); [_swiftAPI setReadForMessage:messageId read:read]; } - (void)setAutoDisplayPaused:(BOOL)autoDisplayPaused { + NSLog(@"ReactNativeSdk setAutoDisplayPaused"); [_swiftAPI setAutoDisplayPaused:autoDisplayPaused]; } - (void)trackEvent:(NSString *)name dataFields:(NSDictionary *)dataFields { + NSLog(@"ReactNativeSdk trackEvent"); [_swiftAPI trackEvent:name dataFields:dataFields]; } @@ -201,12 +195,14 @@ - (void)trackPushOpenWithCampaignId:(NSNumber *)campaignId appAlreadyRunning:(BOOL)appAlreadyRunning dataFields:(NSDictionary *)dataFields { + NSLog(@"ReactNativeSdk trackPushOpenWithCampaignId"); [_swiftAPI trackPushOpenWithCampaignId:campaignId templateId:templateId messageId:messageId appAlreadyRunning:appAlreadyRunning dataFields:dataFields]; } - (void)trackInAppOpen:(NSString *)messageId location:(NSNumber *)location { + NSLog(@"ReactNativeSdk trackInAppOpen"); [_swiftAPI trackInAppOpen:messageId location:location]; } @@ -214,6 +210,7 @@ - (void)trackInAppClick:(NSString *)messageId location:(NSNumber *)location clickedUrl:(NSString *)clickedUrl { + NSLog(@"ReactNativeSdk trackInAppClick"); [_swiftAPI trackInAppClick:messageId location:location clickedUrl:clickedUrl]; } @@ -222,6 +219,7 @@ - (void)trackInAppClose:(NSString *)messageId source:(NSNumber *)source clickedUrl:(NSString *)clickedUrl { + NSLog(@"ReactNativeSdk trackInAppClose"); [_swiftAPI trackInAppClose:messageId location:location source:source clickedUrl:clickedUrl]; } @@ -229,11 +227,13 @@ - (void)inAppConsume:(NSString *)messageId location:(NSNumber *)location source:(NSNumber *)source { + NSLog(@"ReactNativeSdk inAppConsume"); [_swiftAPI inAppConsume:messageId location:location source:source]; } - (void)updateCart:(NSArray *)items { + NSLog(@"ReactNativeSdk updateCart"); [_swiftAPI updateCart:items]; } @@ -241,40 +241,47 @@ - (void)trackPurchase:(NSNumber *)total items:(NSArray *)items dataFields:(NSDictionary *)dataFields { + NSLog(@"ReactNativeSdk trackPurchase"); [_swiftAPI trackPurchase:total items:items dataFields:dataFields]; } - (void)updateUser:(NSDictionary *)dataFields mergeNestedObjects:(BOOL)mergeNestedObjects { + NSLog(@"ReactNativeSdk updateUser"); [_swiftAPI updateUser:dataFields mergeNestedObjects:mergeNestedObjects]; } - (void)updateEmail:(NSString *)email authToken:(NSString *)authToken { + NSLog(@"ReactNativeSdk updateEmail"); [_swiftAPI updateEmail:email authToken:authToken]; } - (void)getAttributionInfo:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + NSLog(@"ReactNativeSdk getAttributionInfo"); [_swiftAPI getAttributionInfo:resolve rejecter:reject]; } - (void)setAttributionInfo:(NSDictionary *)attributionInfo { + NSLog(@"ReactNativeSdk setAttributionInfo"); [_swiftAPI setAttributionInfo:attributionInfo]; } - (void)disableDeviceForCurrentUser { + NSLog(@"ReactNativeSdk disableDeviceForCurrentUser"); [_swiftAPI disableDeviceForCurrentUser]; } - (void)getLastPushPayload:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + NSLog(@"ReactNativeSdk getLastPushPayload"); [_swiftAPI getLastPushPayload:resolve rejecter:reject]; } @@ -282,6 +289,7 @@ - (void)getHtmlInAppContentForMessage:(NSString *)messageId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + NSLog(@"ReactNativeSdk getHtmlInAppContentForMessage"); [_swiftAPI getHtmlInAppContentForMessage:messageId resolver:resolve rejecter:reject]; } @@ -289,43 +297,45 @@ - (void)handleAppLink:(NSString *)appLink resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + NSLog(@"ReactNativeSdk handleAppLink"); [_swiftAPI handleAppLink:appLink resolver:resolve rejecter:reject]; } -- (void)updateSubscriptions:(NSArray * _Nullable)emailListIds - unsubscribedChannelIds:(NSArray * _Nullable)unsubscribedChannelIds - unsubscribedMessageTypeIds:(NSArray * _Nullable)unsubscribedMessageTypeIds - subscribedMessageTypeIds:(NSArray * _Nullable)subscribedMessageTypeIds +- (void)updateSubscriptions:(NSArray *)emailListIds + unsubscribedChannelIds:(NSArray *)unsubscribedChannelIds + unsubscribedMessageTypeIds:(NSArray *)unsubscribedMessageTypeIds + subscribedMessageTypeIds:(NSArray *)subscribedMessageTypeIds campaignId:(NSNumber *)campaignId templateId:(NSNumber *)templateId { + NSLog(@"ReactNativeSdk updateSubscriptions"); [_swiftAPI updateSubscriptions:emailListIds unsubscribedChannelIds:unsubscribedChannelIds unsubscribedMessageTypeIds:unsubscribedMessageTypeIds subscribedMessageTypeIds:subscribedMessageTypeIds campaignId:campaignId templateId:templateId]; } - (void)startSession:(NSArray *)visibleRows { + NSLog(@"ReactNativeSdk startSession"); [_swiftAPI startSession:visibleRows]; } - (void)endSession { + NSLog(@"ReactNativeSdk endSession"); [_swiftAPI endSession]; } - (void)updateVisibleRows:(NSArray *)visibleRows { + NSLog(@"ReactNativeSdk updateVisibleRows"); [_swiftAPI updateVisibleRows:visibleRows]; } - (void)passAlongAuthToken:(NSString *)authToken { + NSLog(@"ReactNativeSdk passAlongAuthToken"); [_swiftAPI passAlongAuthToken:authToken]; } -using namespace facebook::react; - - - - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index 0c3fd3efb..a1a135562 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -6,764 +6,695 @@ import React func sendEvent(withName: String, body: Any?) } -@objc protocol RCTAppContext { - func emit(eventName: String, payload: Any?) -} - -@objc(BridgelessEventEmitterHost) -class BridgelessEventEmitterHost: NSObject { - private let appContext: RCTAppContext - - init(appContext: RCTAppContext) { - self.appContext = appContext - super.init() - NSLog("*** BridgelessEventEmitterHost initialized ***") - } - - func send(name: String, body: Any?) { - appContext.emit(eventName: name, payload: body) - } - - func getShouldEmit() -> Bool { - // Optional logic if you want to control flow - return true - } -} - -class EventDispatcher { - private var bridgelessEmitter: BridgelessEventEmitterHost? - - init(appContext: RCTAppContext) { - NSLog("*** BRIDGELESS EventDispatcher init ***") - self.bridgelessEmitter = BridgelessEventEmitterHost(appContext: appContext) - } - - func send(name: String, body: Any?) { - if let emitter = bridgelessEmitter { - emitter.send(name: name, body: body) - } else { - NSLog("⚠️ No emitter available to send event: \(name)") - } - } - - func getShouldEmit() -> Bool { - return bridgelessEmitter?.getShouldEmit() ?? false - } -} - -@objc(ReactIterableAPI) -public class ReactIterableAPI: NSObject { - @objc public static let shared = ReactIterableAPI() - - private var savedLaunchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - private var eventDispatcher: EventDispatcher? - - override public init() { - super.init() - - NSLog("*** ReactIterableAPI init ***") - } - - init(appContext: RCTAppContext) { - eventDispatcher = EventDispatcher(appContext: appContext) - } - -// @objc public weak var delegate: ReactIterableAPIDelegate? = nil - - // MARK: - React Native Functions - -// @objc override public class func moduleName() -> String! { -// return "RNIterableAPI" -// } - - // IMPORTANT: This is part of legacy event emitter -// override open var methodQueue: DispatchQueue! { -// _methodQueue -// } - -// @objc override public static func requiresMainQueueSetup() -> Bool { -// false -// } - - enum EventName: String, CaseIterable { - case handleUrlCalled - case handleCustomActionCalled - case handleInAppCalled - case handleAuthCalled - case receivedIterableInboxChanged - case handleAuthSuccessCalled - case handleAuthFailureCalled - case onTestEventDispatch - } - - @objc(testEventDispatch) - public func testEventDispatch() { - eventDispatcher?.send(name: EventName.onTestEventDispatch.rawValue, body: 0) - } - - // MARK: - Native SDK Functions - - @objc(initializeWithApiKey:config:version:resolver:rejecter:) - public func initialize(apiKey: String, - config configDict: NSDictionary, - version: String, - resolver: @escaping RCTPromiseResolveBlock, - rejecter: @escaping RCTPromiseRejectBlock) { - ITBInfo() - - initialize(withApiKey: apiKey, - config: configDict, - version: version, - resolver: resolver, - rejecter: rejecter) - } - - @objc(initialize2WithApiKey:config:apiEndPointOverride:version:resolver:rejecter:) - public func initialize2(apiKey: String, - config configDict: NSDictionary, - version: String, - apiEndPointOverride: String, - resolver: @escaping RCTPromiseResolveBlock, - rejecter: @escaping RCTPromiseRejectBlock) { - ITBInfo() - - initialize(withApiKey: apiKey, - config: configDict, - version: version, - apiEndPointOverride: apiEndPointOverride, - resolver: resolver, - rejecter: rejecter) - } - - @objc(setEmail:) - public func set(email: String?) { - ITBInfo() - - IterableAPI.email = email - } - - @objc(setEmail:authToken:) - public func set(email: String?, authToken: String?) { - ITBInfo() - - IterableAPI.setEmail(email, authToken) - } - - @objc(getEmail:rejecter:) - public func getEmail(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { - ITBInfo() - - resolver(IterableAPI.email) - } - - @objc(setUserId:) - public func set(userId: String?) { - ITBInfo() - - IterableAPI.userId = userId - } - - @objc(setUserId:authToken:) - public func set(userId: String?, authToken: String?) { - ITBInfo() - - IterableAPI.setUserId(userId, authToken) - } - - @objc(getUserId:rejecter:) - public func getUserId(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { - ITBInfo() - - resolver(IterableAPI.userId) - } - - // MARK: - Iterable API Request Functions - - @objc(setInAppShowResponse:) - public func set(inAppShowResponse number: NSNumber) { - ITBInfo() - - self.inAppShowResponse = InAppShowResponse.from(number: number) - - inAppHandlerSemaphore.signal() - } - - @objc(disableDeviceForCurrentUser) - public func disableDeviceForCurrentUser() { - ITBInfo() - - IterableAPI.disableDeviceForCurrentUser() - } - - @objc(getLastPushPayload:rejecter:) - public func getLastPushPayload(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { - ITBInfo() - - resolver(IterableAPI.lastPushPayload) - } - - @objc(getAttributionInfo:rejecter:) - public func getAttributionInfo(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { - ITBInfo() - - resolver(IterableAPI.attributionInfo.map(SerializationUtil.encodableToDictionary)) - } - - @objc(setAttributionInfo:) - public func set(attributionInfo dict: NSDictionary?) { - ITBInfo() - - guard let dict = dict else { - IterableAPI.attributionInfo = nil - return - } - - IterableAPI.attributionInfo = SerializationUtil.dictionaryToDecodable(dict: dict as! [AnyHashable: Any]) - } - - @objc(trackPushOpenWithCampaignId:templateId:messageId:appAlreadyRunning:dataFields:) - public func trackPushOpen(campaignId: NSNumber, - templateId: NSNumber?, - messageId: String, - appAlreadyRunning: Bool, - dataFields: NSDictionary?) { - ITBInfo() - - IterableAPI.track(pushOpen: campaignId, - templateId: templateId, - messageId: messageId, - appAlreadyRunning: appAlreadyRunning, - dataFields: dataFields as? [AnyHashable: Any]) - } - - @objc(updateCart:) - public func updateCart(items: [NSDictionary]) { - ITBInfo() - - let swiftItems = items.compactMap { $0 as? [AnyHashable: Any] } - IterableAPI.updateCart(items: swiftItems.compactMap(CommerceItem.from(dict:))) - } - - @objc(trackPurchase:items:dataFields:) - public func trackPurchase(total: NSNumber, - items: [NSDictionary], - dataFields: NSDictionary?) { - ITBInfo() - - let swiftItems = items.compactMap { $0 as? [AnyHashable: Any] } - IterableAPI.track(purchase: total, - items: swiftItems.compactMap(CommerceItem.from(dict:)), - dataFields: dataFields as? [AnyHashable: Any]) - } - - @objc(trackInAppOpen:location:) - public func trackInAppOpen(messageId: String, - location locationNumber: NSNumber) { - ITBInfo() - - guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { - ITBError("Could not find message with id: \(messageId)") - return - } - - IterableAPI.track(inAppOpen: message, location: InAppLocation.from(number: locationNumber)) - } - - @objc(trackInAppClick:location:clickedUrl:) - public func trackInAppClick(messageId: String, - location locationNumber: NSNumber, - clickedUrl: String) { - ITBInfo() - - guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { - ITBError("Could not find message with id: \(messageId)") - return - } - - IterableAPI.track(inAppClick: message, location: InAppLocation.from(number: locationNumber), clickedUrl: clickedUrl) - } - - @objc(trackInAppClose:location:source:clickedUrl:) - public func trackInAppClose(messageId: String, - location locationNumber: NSNumber, - source sourceNumber: NSNumber, - clickedUrl: String?) { - ITBInfo() - - guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { - ITBError("Could not find message with id: \(messageId)") - return - } - - if let inAppCloseSource = InAppCloseSource.from(number: sourceNumber) { - IterableAPI.track(inAppClose: message, - location: InAppLocation.from(number: locationNumber), - source: inAppCloseSource, - clickedUrl: clickedUrl) - } else { - IterableAPI.track(inAppClose: message, - location: InAppLocation.from(number: locationNumber), - clickedUrl: clickedUrl) - } - } - - @objc(inAppConsume:location:source:) - public func inAppConsume(messageId: String, - location locationNumber: NSNumber, - source sourceNumber: NSNumber) { - ITBInfo() - - guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { - ITBError("Could not find message with id: \(messageId)") - return - } - - if let inAppDeleteSource = InAppDeleteSource.from(number: sourceNumber) { - IterableAPI.inAppConsume(message: message, - location: InAppLocation.from(number: locationNumber), - source: inAppDeleteSource) - } else { - IterableAPI.inAppConsume(message: message, - location: InAppLocation.from(number: locationNumber)) - } - } - - @objc(getHtmlInAppContentForMessage:resolver:rejecter:) - public func getHtmlInAppContent(messageId: String, resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { - ITBInfo() - - guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { - ITBError("Could not find message with id: \(messageId)") - rejecter("", "Could not find message with id: \(messageId)", nil) - return - } - - guard let content = message.content as? IterableHtmlInAppContent else { - ITBError("Could not parse message content as HTML") - rejecter("", "Could not parse message content as HTML", nil) - return - } - - resolver(content.toDict()) - } - - @objc(trackEvent:dataFields:) - public func trackEvent(name: String, dataFields: NSDictionary?) { - ITBInfo() - - IterableAPI.track(event: name, dataFields: dataFields as? [AnyHashable: Any]) - } - - @objc(updateUser:mergeNestedObjects:) - public func updateUser(dataFields: NSDictionary, mergeNestedObjects: Bool) { - ITBInfo() - - IterableAPI - .updateUser( - dataFields as! [AnyHashable: Any], - mergeNestedObjects: mergeNestedObjects - ) - } - - @objc(updateEmail:authToken:) - public func updateEmail(email: String, with authToken: String?) { - ITBInfo() - - if let authToken = authToken { - IterableAPI.updateEmail(email, withToken: authToken, onSuccess: nil, onFailure: nil) - } else { - IterableAPI.updateEmail(email, onSuccess: nil, onFailure: nil) - } - } - - @objc(handleAppLink:resolver:rejecter:) - public func handle(appLink: String, resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { - ITBInfo() - - if let url = URL(string: appLink) { - resolver(IterableAPI.handle(universalLink: url)) - } else { - rejecter("", "invalid URL", nil) - } - } - - // MARK: - SDK In-App Manager Functions - - @objc(getInAppMessages:rejecter:) - public func getInAppMessages(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { - ITBInfo() - - resolver(IterableAPI.inAppManager.getMessages().map { $0.toDict() }) - } - - @objc(getInboxMessages:rejecter:) - public func getInboxMessages(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { - ITBInfo() - - resolver(IterableAPI.inAppManager.getInboxMessages().map{ $0.toDict() }) - } - - @objc(getUnreadInboxMessagesCount:rejecter:) - public func getUnreadInboxMessagesCount(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { - ITBInfo() - - resolver(IterableAPI.inAppManager.getUnreadInboxMessagesCount()) - } - - @objc(showMessage:consume:resolver:rejecter:) - public func show(messageId: String, consume: Bool, resolver: @escaping RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { - ITBInfo() - - guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { - ITBError("Could not find message with id: \(messageId)") - return - } - - IterableAPI.inAppManager.show(message: message, consume: consume) { (url) in - resolver(url.map({$0.absoluteString})) - } - } - - @objc(removeMessage:location:source:) - public func remove(messageId: String, location locationNumber: NSNumber, source sourceNumber: NSNumber) { - ITBInfo() - - guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { - ITBError("Could not find message with id: \(messageId)") - return - } - - if let inAppDeleteSource = InAppDeleteSource.from(number: sourceNumber) { - IterableAPI.inAppManager.remove(message: message, - location: InAppLocation.from(number: locationNumber), - source: inAppDeleteSource) - } else { - IterableAPI.inAppManager.remove(message: message, - location: InAppLocation.from(number: locationNumber)) - } - } - - @objc(updateSubscriptions:unsubscribedChannelIds:unsubscribedMessageTypeIds:subscribedMessageTypeIds:campaignId:templateId:) - public func updateSubscriptions(emailListIds: [NSNumber]?, - unsubscribedChannelIds: [NSNumber]?, - unsubscribedMessageTypeIds: [NSNumber]?, - subscribedMessageTypeIds: [NSNumber]?, - campaignId: NSNumber, - templateId: NSNumber) { - ITBInfo() - - let finalCampaignId: NSNumber? = campaignId.intValue <= 0 ? nil : campaignId - let finalTemplateId: NSNumber? = templateId.intValue <= 0 ? nil : templateId - - IterableAPI.updateSubscriptions(emailListIds, - unsubscribedChannelIds: unsubscribedChannelIds, - unsubscribedMessageTypeIds: unsubscribedMessageTypeIds, - subscribedMessageTypeIds: subscribedMessageTypeIds, - campaignId: finalCampaignId, - templateId: finalTemplateId) - } - - @objc(setReadForMessage:read:) - public func setRead(for messageId: String, read: Bool) { - ITBInfo() - - guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { - ITBError("Could not find message with id: \(messageId)") - return - } - - IterableAPI.inAppManager.set(read: read, forMessage: message) - } - - @objc(setAutoDisplayPaused:) - public func set(autoDisplayPaused: Bool) { - ITBInfo() - - DispatchQueue.main.async { - IterableAPI.inAppManager.isAutoDisplayPaused = autoDisplayPaused - } - } - - // MARK: - SDK Inbox Session Tracking Functions - - @objc(startSession:) - public func startSession(visibleRows: [NSDictionary]) { - let serializedRows = InboxImpressionTracker.RowInfo.rowInfos(from: visibleRows as! [[AnyHashable: Any]]) - - inboxSessionManager.startSession(visibleRows: serializedRows) - } - - @objc(endSession) - public func endSession() { - guard let sessionInfo = inboxSessionManager.endSession() else { - ITBError("Could not find session info") - return - } - - let inboxSession = IterableInboxSession(id: sessionInfo.startInfo.id, - sessionStartTime: sessionInfo.startInfo.startTime, - sessionEndTime: Date(), - startTotalMessageCount: sessionInfo.startInfo.totalMessageCount, - startUnreadMessageCount: sessionInfo.startInfo.unreadMessageCount, - endTotalMessageCount: IterableAPI.inAppManager.getInboxMessages().count, - endUnreadMessageCount: IterableAPI.inAppManager.getUnreadInboxMessagesCount(), - impressions: sessionInfo.impressions.map { $0.toIterableInboxImpression() }) - - IterableAPI.track(inboxSession: inboxSession) - } - - @objc(updateVisibleRows:) - public func updateVisibleRows(visibleRows: [NSDictionary]) { - let serializedRows = InboxImpressionTracker.RowInfo.rowInfos(from: visibleRows as! [[AnyHashable: Any]]) - - inboxSessionManager.updateVisibleRows(visibleRows: serializedRows) - } - - // MARK: - SDK Auth Manager Functions - - @objc(passAlongAuthToken:) - public func passAlong(authToken: String?) { - ITBInfo() - - passedAuthToken = authToken - - authHandlerSemaphore.signal() - } - - // MARK: Private - private var shouldEmit = false - private let _methodQueue = DispatchQueue(label: String(describing: ReactIterableAPI.self)) - - // Handling in-app delegate - private var inAppShowResponse = InAppShowResponse.show - private var inAppHandlerSemaphore = DispatchSemaphore(value: 0) - - private var passedAuthToken: String? - private var authHandlerSemaphore = DispatchSemaphore(value: 0) - - private let inboxSessionManager = InboxSessionManager() - - @objc func initialize(withApiKey apiKey: String, - config configDict: NSDictionary, - version: String, - apiEndPointOverride: String? = nil, - resolver: @escaping RCTPromiseResolveBlock, - rejecter: @escaping RCTPromiseRejectBlock) { - ITBInfo() - - let launchOptions = createLaunchOptions() - NSLog("*** launchOptions: \(launchOptions) ***") - let iterableConfig = IterableConfig.from( - dict: configDict as? [AnyHashable: Any] - ) - - if let urlHandlerPresent = configDict["urlHandlerPresent"] as? Bool, urlHandlerPresent == true { - iterableConfig.urlDelegate = self - } - - if let customActionHandlerPresent = configDict["customActionHandlerPresent"] as? Bool, customActionHandlerPresent == true { - iterableConfig.customActionDelegate = self - } - - if let inAppHandlerPresent = configDict["inAppHandlerPresent"] as? Bool, inAppHandlerPresent == true { - iterableConfig.inAppDelegate = self - } - - if let authHandlerPresent = configDict["authHandlerPresent"] as? Bool, authHandlerPresent { - iterableConfig.authDelegate = self - } - - // connect new inbox in-app payloads to the RN SDK - NotificationCenter.default.addObserver(self, selector: #selector(receivedIterableInboxChanged), name: Notification.Name.iterableInboxChanged, object: nil) - - DispatchQueue.main.async { - IterableAPI.initialize2(apiKey: apiKey, - launchOptions: launchOptions, - config: iterableConfig, - apiEndPointOverride: apiEndPointOverride) { result in - resolver(result) - } - - IterableAPI.setDeviceAttribute(name: "reactNativeSDKVersion", value: version) - } - } - - @objc(receivedIterableInboxChanged) - public func receivedIterableInboxChanged() { - NSLog("*** receivedIterableInboxChanged1 ***") -// guard shouldEmit else { -// return -// } - - NSLog("***SEND EVENT***: receivedIterableInboxChanged") - - eventDispatcher?.send(name: EventName.receivedIterableInboxChanged.rawValue, body: nil) -// sendEvent(withName: EventName.receivedIterableInboxChanged.rawValue, body: nil) - } - - private func createLaunchOptions() -> [UIApplication.LaunchOptionsKey: Any]? { - return nil - // new‐arch path: use what we captured -// if let opts = savedLaunchOptions { -// return ReactIterableAPI.createLaunchOptions(bridgeLaunchOptions: opts) -// } - - // legacy‐bridge path left in if you’re still building for RCTBridge -// if let legacy = bridge?.launchOptions { -// return ReactIterableAPI.createLaunchOptions(bridgeLaunchOptions: legacy) -// } - -// return nil - // guard let bridge = self.bridge else { - // return nil - // } - - // return ReactIterableAPI.createLaunchOptions(bridgeLaunchOptions: bridge.launchOptions) - } - - private static func createLaunchOptions(bridgeLaunchOptions: [AnyHashable: Any]?) -> [UIApplication.LaunchOptionsKey: Any]? { - NSLog("*** createLaunchOptions2 ***: \(bridgeLaunchOptions)") - guard let bridgeLaunchOptions = bridgeLaunchOptions, - let remoteNotification = bridgeLaunchOptions[UIApplication.LaunchOptionsKey.remoteNotification.rawValue] else { - return nil - } - - var result = [UIApplication.LaunchOptionsKey: Any]() - result[UIApplication.LaunchOptionsKey.remoteNotification] = remoteNotification - - return result - } +@objc public class ReactIterableAPI: RCTEventEmitter { + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc public weak var delegate: ReactIterableAPIDelegate? = nil + + @objc override public class func moduleName() -> String! { + return "RNIterableAPI" + } + + override open var methodQueue: DispatchQueue! { + _methodQueue + } + + @objc override static public func requiresMainQueueSetup() -> Bool { + false + } + + enum EventName: String, CaseIterable { + case handleUrlCalled + case handleCustomActionCalled + case handleInAppCalled + case handleAuthCalled + case receivedIterableInboxChanged + case handleAuthSuccessCalled + case handleAuthFailureCalled + case onTestEventDispatch + } + + @objc public static var supportedEvents: [String] { + return EventName.allCases.map(\.rawValue) + } + + override public func startObserving() { + ITBInfo() + + shouldEmit = true + } + + override public func stopObserving() { + ITBInfo() + + shouldEmit = false + } + + // MARK: - Native SDK Functions + + @objc public func hello() { + print("Hello from Swift Again") + } + + @objc(initializeWithApiKey:config:version:resolver:rejecter:) + public func initializeWithApiKey( + apiKey: String, + config configDict: NSDictionary, + version: String, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + NSLog("initializeWithApiKey called from swift") + ITBInfo() + + initialize( + withApiKey: apiKey, + config: configDict, + version: version, + resolver: resolver, + rejecter: rejecter) + } + + @objc(initialize2WithApiKey:config:apiEndPointOverride:version:resolver:rejecter:) + public func initialize2WithApiKey( + apiKey: String, + config configDict: NSDictionary, + version: String, + apiEndPointOverride: String, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + ITBInfo() + + initialize( + withApiKey: apiKey, + config: configDict, + version: version, + apiEndPointOverride: apiEndPointOverride, + resolver: resolver, + rejecter: rejecter) + } + + @objc(setEmail:) + public func setEmail(email: String?) { + ITBInfo() + IterableAPI.email = email + } + + @objc(setEmail:authToken:) + public func setEmail(email: String?, authToken: String?) { + ITBInfo() + IterableAPI.setEmail(email, authToken) + } + + @objc(getEmail:rejecter:) + public func getEmail(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + ITBInfo() + resolver(IterableAPI.email) + } + + @objc(setUserId:) + public func setUserId(userId: String?) { + ITBInfo() + IterableAPI.userId = userId + } + + @objc(setUserId:authToken:) + public func setUserId(userId: String?, authToken: String?) { + ITBInfo() + IterableAPI.setUserId(userId, authToken) + } + + @objc(getUserId:rejecter:) + public func getUserId(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + ITBInfo() + resolver(IterableAPI.userId) + } + + // MARK: - Iterable API Request Functions + + @objc(setInAppShowResponse:) + public func setInAppShowResponse(inAppShowResponse number: NSNumber) { + ITBInfo() + self.inAppShowResponse = InAppShowResponse.from(number: number) + inAppHandlerSemaphore.signal() + } + + @objc(disableDeviceForCurrentUser) + public func disableDeviceForCurrentUser() { + ITBInfo() + IterableAPI.disableDeviceForCurrentUser() + } + + @objc(getLastPushPayload:rejecter:) + public func getLastPushPayload(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) + { + ITBInfo() + resolver(IterableAPI.lastPushPayload) + } + + @objc(getAttributionInfo:rejecter:) + public func getAttributionInfo(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) + { + ITBInfo() + resolver(IterableAPI.attributionInfo.map(SerializationUtil.encodableToDictionary)) + } + + @objc(setAttributionInfo:) + public func setAttributionInfo(attributionInfo dict: NSDictionary?) { + ITBInfo() + guard let dict = dict else { + IterableAPI.attributionInfo = nil + return + } + IterableAPI.attributionInfo = SerializationUtil.dictionaryToDecodable( + dict: dict as! [AnyHashable: Any]) + } + + @objc(trackPushOpenWithCampaignId:templateId:messageId:appAlreadyRunning:dataFields:) + public func trackPushOpenWithCampaignId( + campaignId: NSNumber, + templateId: NSNumber?, + messageId: String, + appAlreadyRunning: Bool, + dataFields: NSDictionary? + ) { + ITBInfo() + let swiftDict = dataFields as? [AnyHashable: Any] + + IterableAPI.track( + pushOpen: campaignId, + templateId: templateId, + messageId: messageId, + appAlreadyRunning: appAlreadyRunning, + dataFields: swiftDict) + } + + @objc(updateCart:) + public func updateCart(items: [[AnyHashable: Any]]) { + ITBInfo() + IterableAPI.updateCart(items: items.compactMap(CommerceItem.from(dict:))) + } + + @objc(trackPurchase:items:dataFields:) + public func trackPurchase( + total: NSNumber, + items: [[AnyHashable: Any]], + dataFields: [AnyHashable: Any]? + ) { + ITBInfo() + IterableAPI.track( + purchase: total, + items: items.compactMap(CommerceItem.from(dict:)), + dataFields: dataFields) + } + + @objc(trackInAppOpen:location:) + public func trackInAppOpen( + messageId: String, + location locationNumber: NSNumber + ) { + ITBInfo() + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { + ITBError("Could not find message with id: \(messageId)") + return + } + IterableAPI.track(inAppOpen: message, location: InAppLocation.from(number: locationNumber)) + } + + @objc(trackInAppClick:location:clickedUrl:) + public func trackInAppClick( + messageId: String, + location locationNumber: NSNumber, + clickedUrl: String + ) { + ITBInfo() + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { + ITBError("Could not find message with id: \(messageId)") + return + } + IterableAPI.track( + inAppClick: message, location: InAppLocation.from(number: locationNumber), + clickedUrl: clickedUrl) + } + + @objc(trackInAppClose:location:source:clickedUrl:) + public func trackInAppClose( + messageId: String, + location locationNumber: NSNumber, + source sourceNumber: NSNumber, + clickedUrl: String? + ) { + ITBInfo() + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { + ITBError("Could not find message with id: \(messageId)") + return + } + if let inAppCloseSource = InAppCloseSource.from(number: sourceNumber) { + IterableAPI.track( + inAppClose: message, + location: InAppLocation.from(number: locationNumber), + source: inAppCloseSource, + clickedUrl: clickedUrl) + } else { + IterableAPI.track( + inAppClose: message, + location: InAppLocation.from(number: locationNumber), + clickedUrl: clickedUrl) + } + } + + @objc(inAppConsume:location:source:) + public func inAppConsume( + messageId: String, + location locationNumber: NSNumber, + source sourceNumber: NSNumber + ) { + ITBInfo() + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { + ITBError("Could not find message with id: \(messageId)") + return + } + if let inAppDeleteSource = InAppDeleteSource.from(number: sourceNumber) { + IterableAPI.inAppConsume( + message: message, + location: InAppLocation.from(number: locationNumber), + source: inAppDeleteSource) + } else { + IterableAPI.inAppConsume( + message: message, + location: InAppLocation.from(number: locationNumber)) + } + } + + @objc(getHtmlInAppContentForMessage:resolver:rejecter:) + public func getHtmlInAppContent( + messageId: String, resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock + ) { + ITBInfo() + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { + ITBError("Could not find message with id: \(messageId)") + rejecter( + "", "Could not find message with id: \(messageId)", + NSError(domain: "", code: 0, userInfo: nil)) + return + } + guard let content = message.content as? IterableHtmlInAppContent else { + ITBError("Could not parse message content as HTML") + rejecter( + "", "Could not parse message content as HTML", NSError(domain: "", code: 0, userInfo: nil)) + return + } + resolver(content.toDict()) + } + + @objc(trackEvent:dataFields:) + public func trackEvent(name: String, dataFields: NSDictionary?) { + ITBInfo() + + IterableAPI.track(event: name, dataFields: dataFields as? [AnyHashable: Any]) + } + + @objc(updateUser:mergeNestedObjects:) + public func updateUser(dataFields: NSDictionary, mergeNestedObjects: Bool) { + ITBInfo() + IterableAPI.updateUser( + (dataFields as? [AnyHashable: Any])!, mergeNestedObjects: mergeNestedObjects) + } + + @objc(updateEmail:authToken:) + public func updateEmail(email: String, with authToken: String?) { + ITBInfo() + if let authToken = authToken { + IterableAPI.updateEmail(email, withToken: authToken, onSuccess: nil, onFailure: nil) + } else { + IterableAPI.updateEmail(email, onSuccess: nil, onFailure: nil) + } + } + + @objc(handleAppLink:resolver:rejecter:) + public func handle( + appLink: String, resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock + ) { + ITBInfo() + if let url = URL(string: appLink) { + resolver(IterableAPI.handle(universalLink: url)) + } else { + rejecter("", "invalid URL", NSError(domain: "", code: 0, userInfo: nil)) + } + } + + // MARK: - SDK In-App Manager Functions + + @objc(getInAppMessages:rejecter:) + public func getInAppMessages(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + ITBInfo() + resolver(IterableAPI.inAppManager.getMessages().map { $0.toDict() }) + } + + @objc(getInboxMessages:rejecter:) + public func getInboxMessages(resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + ITBInfo() + resolver(IterableAPI.inAppManager.getInboxMessages().map { $0.toDict() }) + } + + @objc(getUnreadInboxMessagesCount:rejecter:) + public func getUnreadInboxMessagesCount( + resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock + ) { + ITBInfo() + resolver(IterableAPI.inAppManager.getUnreadInboxMessagesCount()) + } + + @objc(showMessage:consume:resolver:rejecter:) + public func showMessage( + messageId: String, consume: Bool, resolver: @escaping RCTPromiseResolveBlock, + rejecter: RCTPromiseRejectBlock + ) { + ITBInfo() + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { + ITBError("Could not find message with id: \(messageId)") + return + } + IterableAPI.inAppManager.show(message: message, consume: consume) { (url) in + resolver(url.map({ $0.absoluteString })) + } + } + + @objc(removeMessage:location:source:) + public func removeMessage( + messageId: String, location locationNumber: NSNumber, source sourceNumber: NSNumber + ) { + ITBInfo() + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { + ITBError("Could not find message with id: \(messageId)") + return + } + if let inAppDeleteSource = InAppDeleteSource.from(number: sourceNumber) { + IterableAPI.inAppManager.remove( + message: message, + location: InAppLocation.from(number: locationNumber), + source: inAppDeleteSource) + } else { + IterableAPI.inAppManager.remove( + message: message, + location: InAppLocation.from(number: locationNumber)) + } + } + + @objc( + updateSubscriptions:unsubscribedChannelIds:unsubscribedMessageTypeIds:subscribedMessageTypeIds: + campaignId:templateId: + ) + public func updateSubscriptions( + emailListIds: [NSNumber]?, + unsubscribedChannelIds: [NSNumber]?, + unsubscribedMessageTypeIds: [NSNumber]?, + subscribedMessageTypeIds: [NSNumber]?, + campaignId: NSNumber, + templateId: NSNumber + ) { + ITBInfo() + let finalCampaignId: NSNumber? = campaignId.intValue <= 0 ? nil : campaignId + let finalTemplateId: NSNumber? = templateId.intValue <= 0 ? nil : templateId + IterableAPI.updateSubscriptions( + emailListIds, + unsubscribedChannelIds: unsubscribedChannelIds, + unsubscribedMessageTypeIds: unsubscribedMessageTypeIds, + subscribedMessageTypeIds: subscribedMessageTypeIds, + campaignId: finalCampaignId, + templateId: finalTemplateId) + } + + @objc(setReadForMessage:read:) + public func setReadForMessage(for messageId: String, read: Bool) { + ITBInfo() + guard let message = IterableAPI.inAppManager.getMessage(withId: messageId) else { + ITBError("Could not find message with id: \(messageId)") + return + } + IterableAPI.inAppManager.set(read: read, forMessage: message) + } + + @objc(setAutoDisplayPaused:) + public func setAutoDisplayPaused(autoDisplayPaused: Bool) { + ITBInfo() + DispatchQueue.main.async { + IterableAPI.inAppManager.isAutoDisplayPaused = autoDisplayPaused + } + } + + // MARK: - SDK Inbox Session Tracking Functions + + @objc(startSession:) + public func startSession(visibleRows: [[AnyHashable: Any]]) { + let serializedRows = InboxImpressionTracker.RowInfo.rowInfos(from: visibleRows) + inboxSessionManager.startSession(visibleRows: serializedRows) + } + + @objc(endSession) + public func endSession() { + guard let sessionInfo = inboxSessionManager.endSession() else { + ITBError("Could not find session info") + return + } + let inboxSession = IterableInboxSession( + id: sessionInfo.startInfo.id, + sessionStartTime: sessionInfo.startInfo.startTime, + sessionEndTime: Date(), + startTotalMessageCount: sessionInfo.startInfo.totalMessageCount, + startUnreadMessageCount: sessionInfo.startInfo.unreadMessageCount, + endTotalMessageCount: IterableAPI.inAppManager.getInboxMessages().count, + endUnreadMessageCount: IterableAPI.inAppManager.getUnreadInboxMessagesCount(), + impressions: sessionInfo.impressions.map { $0.toIterableInboxImpression() }) + IterableAPI.track(inboxSession: inboxSession) + } + + @objc(updateVisibleRows:) + public func updateVisibleRows(visibleRows: [[AnyHashable: Any]]) { + let serializedRows = InboxImpressionTracker.RowInfo.rowInfos(from: visibleRows) + inboxSessionManager.updateVisibleRows(visibleRows: serializedRows) + } + + @objc(testEventDispatch) + public func testEventDispatch() { + NSLog("***ITBL SWIFT*** shouldEmit: \(shouldEmit)") + NSLog("***ITBL SWIFT*** testEventDispatch", EventName.onTestEventDispatch.rawValue) + delegate?.sendEvent(withName: EventName.onTestEventDispatch.rawValue, body: 0) + } + + // MARK: - SDK Auth Manager Functions + + @objc(passAlongAuthToken:) + public func passAlongAuthToken(authToken: String?) { + ITBInfo() + passedAuthToken = authToken + authHandlerSemaphore.signal() + } + + // MARK: Private + private var shouldEmit = false + private let _methodQueue = DispatchQueue(label: String(describing: ReactIterableAPI.self)) + + // Handling in-app delegate + private var inAppShowResponse = InAppShowResponse.show + private var inAppHandlerSemaphore = DispatchSemaphore(value: 0) + + private var passedAuthToken: String? + private var authHandlerSemaphore = DispatchSemaphore(value: 0) + + private let inboxSessionManager = InboxSessionManager() + + @objc func initialize( + withApiKey apiKey: String, + config configDict: NSDictionary, + version: String, + apiEndPointOverride: String? = nil, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + ITBInfo() + let launchOptions = createLaunchOptions() + let iterableConfig = IterableConfig.from( + dict: configDict as? [AnyHashable : Any] + ) + + if let urlHandlerPresent = configDict["urlHandlerPresent"] as? Bool, urlHandlerPresent == true { + iterableConfig.urlDelegate = self + } + + if let customActionHandlerPresent = configDict["customActionHandlerPresent"] as? Bool, + customActionHandlerPresent == true + { + iterableConfig.customActionDelegate = self + } + + if let inAppHandlerPresent = configDict["inAppHandlerPresent"] as? Bool, + inAppHandlerPresent == true + { + iterableConfig.inAppDelegate = self + } + + if let authHandlerPresent = configDict["authHandlerPresent"] as? Bool, authHandlerPresent { + iterableConfig.authDelegate = self + } + + // connect new inbox in-app payloads to the RN SDK + NotificationCenter.default.addObserver( + self, selector: #selector(receivedIterableInboxChanged), + name: Notification.Name.iterableInboxChanged, object: nil) + + DispatchQueue.main.async { + IterableAPI.initialize2( + apiKey: apiKey, + launchOptions: launchOptions, + config: iterableConfig, + apiEndPointOverride: apiEndPointOverride + ) { result in + resolver(result) + } + + IterableAPI.setDeviceAttribute(name: "reactNativeSDKVersion", value: version) + } + } + + @objc(receivedIterableInboxChanged) + func receivedIterableInboxChanged() { + guard shouldEmit else { + return + } + delegate?.sendEvent( + withName: EventName.receivedIterableInboxChanged.rawValue, body: nil as Any?) + } + + private func createLaunchOptions() -> [UIApplication.LaunchOptionsKey: Any]? { + guard let bridge = self.bridge else { + return nil + } + return ReactIterableAPI.createLaunchOptions(bridgeLaunchOptions: bridge.launchOptions) + } + + private static func createLaunchOptions(bridgeLaunchOptions: [AnyHashable: Any]?) + -> [UIApplication.LaunchOptionsKey: Any]? + { + guard let bridgeLaunchOptions = bridgeLaunchOptions, + let remoteNotification = bridgeLaunchOptions[ + UIApplication.LaunchOptionsKey.remoteNotification.rawValue] + else { + return nil + } + var result = [UIApplication.LaunchOptionsKey: Any]() + result[UIApplication.LaunchOptionsKey.remoteNotification] = remoteNotification + return result + } } extension ReactIterableAPI: IterableURLDelegate { - public func handle(iterableURL url: URL, inContext context: IterableActionContext) -> Bool { - ITBInfo() - -// guard shouldEmit else { -// return false -// } - - let contextDict = ReactIterableAPI.contextToDictionary(context: context) - - NSLog("***SEND EVENT***: handle(iterableURL url: \(url.absoluteString), inContext context: \(contextDict)") - - eventDispatcher?.send(name: EventName.handleUrlCalled.rawValue, - body: ["url": url.absoluteString, - "context": contextDict] as [String : Any]) - - return true - } - - private static func contextToDictionary(context: IterableActionContext) -> [AnyHashable: Any] { - var result = [AnyHashable: Any]() - - let actionDict = actionToDictionary(action: context.action) - result["action"] = actionDict - result["source"] = context.source.rawValue - - return result - } - - private static func actionToDictionary(action: IterableAction) -> [AnyHashable: Any] { - var actionDict = [AnyHashable: Any]() - - actionDict["type"] = action.type - - if let data = action.data { - actionDict["data"] = data - } - - if let userInput = action.userInput { - actionDict["userInput"] = userInput - } - - return actionDict - } + public func handle(iterableURL url: URL, inContext context: IterableActionContext) -> Bool { + ITBInfo() + guard shouldEmit else { + return false + } + let contextDict = ReactIterableAPI.contextToDictionary(context: context) + delegate?.sendEvent( + withName: EventName.handleUrlCalled.rawValue, + body: [ + "url": url.absoluteString, + "context": contextDict, + ] as [String: Any]) + return true + } + + private static func contextToDictionary(context: IterableActionContext) -> [AnyHashable: Any] { + var result = [AnyHashable: Any]() + let actionDict = actionToDictionary(action: context.action) + result["action"] = actionDict + result["source"] = context.source.rawValue + return result + } + + private static func actionToDictionary(action: IterableAction) -> [AnyHashable: Any] { + var actionDict = [AnyHashable: Any]() + actionDict["type"] = action.type + if let data = action.data { + actionDict["data"] = data + } + if let userInput = action.userInput { + actionDict["userInput"] = userInput + } + return actionDict + } } extension ReactIterableAPI: IterableCustomActionDelegate { - public func handle(iterableCustomAction action: IterableAction, inContext context: IterableActionContext) -> Bool { - ITBInfo() - - let actionDict = ReactIterableAPI.actionToDictionary(action: action) - let contextDict = ReactIterableAPI.contextToDictionary(context: context) - - NSLog("***SEND EVENT***: handle(iterableCustomAction action: \(actionDict), inContext context: \(contextDict)") - - eventDispatcher?.send(name: EventName.handleCustomActionCalled.rawValue, - body: ["action": actionDict, - "context": contextDict]) - - return true - } + public func handle( + iterableCustomAction action: IterableAction, inContext context: IterableActionContext + ) + -> Bool + { + ITBInfo() + let actionDict = ReactIterableAPI.actionToDictionary(action: action) + let contextDict = ReactIterableAPI.contextToDictionary(context: context) + delegate?.sendEvent( + withName: EventName.handleCustomActionCalled.rawValue, + body: [ + "action": actionDict, + "context": contextDict, + ]) + return true + } } extension ReactIterableAPI: IterableInAppDelegate { - public func onNew(message: IterableInAppMessage) -> InAppShowResponse { - ITBInfo() - -// guard shouldEmit else { -// return .show -// } - - NSLog("***SEND EVENT***: onNew(message: \(message.toDict())") - - eventDispatcher?.send(name: EventName.handleInAppCalled.rawValue, - body: message.toDict()) - - let timeoutResult = inAppHandlerSemaphore.wait(timeout: .now() + 2.0) - - if timeoutResult == .success { - ITBInfo("inAppShowResponse: \(inAppShowResponse == .show)") - return inAppShowResponse - } else { - ITBInfo("timed out") - return .show - } - } + public func onNew(message: IterableInAppMessage) -> InAppShowResponse { + ITBInfo() + guard shouldEmit else { + return .show + } + delegate?.sendEvent( + withName: EventName.handleInAppCalled.rawValue, + body: message.toDict()) + let timeoutResult = inAppHandlerSemaphore.wait(timeout: .now() + 2.0) + if timeoutResult == .success { + ITBInfo("inAppShowResponse: \(inAppShowResponse == .show)") + return inAppShowResponse + } else { + ITBInfo("timed out") + return .show + } + } } extension ReactIterableAPI: IterableAuthDelegate { - public func onAuthTokenRequested(completion: @escaping AuthTokenRetrievalHandler) { - ITBInfo() - - DispatchQueue.global(qos: .userInitiated).async { - NSLog("***SEND EVENT***: onAuthTokenRequested") - - self.eventDispatcher?.send(name: EventName.handleAuthCalled.rawValue, - body: nil as Any?) - - let authTokenRetrievalResult = self.authHandlerSemaphore.wait(timeout: .now() + 30.0) - - if authTokenRetrievalResult == .success { - ITBInfo("authTokenRetrieval successful") - - DispatchQueue.main.async { - completion(self.passedAuthToken) - } - - NSLog("***SEND EVENT***: handleAuthSuccessCalled") - - self.eventDispatcher? - .send(name: EventName.handleAuthSuccessCalled.rawValue, - body: nil as Any?) - } else { - ITBInfo("authTokenRetrieval timed out") - - DispatchQueue.main.async { - completion(nil) - } - - NSLog("***SEND EVENT***: handleAuthFailureCalled") - - self.eventDispatcher? - .send(name: EventName.handleAuthFailureCalled.rawValue, - body: nil as Any?) - } + public func onAuthTokenRequested(completion: @escaping AuthTokenRetrievalHandler) { + ITBInfo() + DispatchQueue.global(qos: .userInitiated).async { + self.delegate?.sendEvent( + withName: EventName.handleAuthCalled.rawValue, + body: nil as Any?) + let authTokenRetrievalResult = self.authHandlerSemaphore.wait(timeout: .now() + 30.0) + if authTokenRetrievalResult == .success { + ITBInfo("authTokenRetrieval successful") + DispatchQueue.main.async { + completion(self.passedAuthToken) } + self.delegate?.sendEvent( + withName: EventName.handleAuthSuccessCalled.rawValue, + body: nil as Any?) + } else { + ITBInfo("authTokenRetrieval timed out") + DispatchQueue.main.async { + completion(nil) + } + self.delegate?.sendEvent( + withName: EventName.handleAuthFailureCalled.rawValue, + body: nil as Any?) + } } + } - public func onTokenRegistrationFailed(_ reason: String?) { - - } + public func onTokenRegistrationFailed(_ reason: String?) { + } } diff --git a/package.json b/package.json index ebfd25a20..de88c62cf 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,9 @@ "devDependencies": { "@commitlint/config-conventional": "^19.6.0", "@evilmartians/lefthook": "^1.5.0", + "@react-native-community/cli": "18.0.0", + "@react-native-community/cli-platform-android": "18.0.0", + "@react-native-community/cli-platform-ios": "18.0.0", "@react-native/babel-preset": "0.79.3", "@react-native/eslint-config": "0.79.3", "@react-native/metro-config": "0.79.3", diff --git a/src/api/index.ts b/src/api/index.ts index aee4b4e85..7e744ee67 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,15 +1,7 @@ /* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports */ import { NativeModules } from 'react-native'; +import BridgelessModule from './NativeRNIterableAPI'; -export const isTurboModuleEnabled = global.__turboModuleProxy != null; - -const getNativeModule = () => { - if (isTurboModuleEnabled) { - return require('./NativeRNIterableAPI').default; - } - return NativeModules.RNIterableAPI; -}; - -export const RNIterableAPI = getNativeModule(); +export const RNIterableAPI = BridgelessModule ?? NativeModules.RNIterableAPI; export default RNIterableAPI; diff --git a/src/global.d.ts b/src/global.d.ts deleted file mode 100644 index 3cd4060e3..000000000 --- a/src/global.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare const global: { - __turboModuleProxy?: (moduleName: string) => object | undefined; -}; diff --git a/yarn.lock b/yarn.lock index 241f48812..722390fcc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3107,6 +3107,9 @@ __metadata: dependencies: "@commitlint/config-conventional": ^19.6.0 "@evilmartians/lefthook": ^1.5.0 + "@react-native-community/cli": 18.0.0 + "@react-native-community/cli-platform-android": 18.0.0 + "@react-native-community/cli-platform-ios": 18.0.0 "@react-native/babel-preset": 0.79.3 "@react-native/eslint-config": 0.79.3 "@react-native/metro-config": 0.79.3 From cdf438d99007eca7c5727f554eea434613958bb5 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 1 Aug 2025 08:38:15 -0700 Subject: [PATCH 7/8] refactor: remove unused eslint disable comments from index.ts --- src/api/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/index.ts b/src/api/index.ts index 7e744ee67..9c327891b 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports */ import { NativeModules } from 'react-native'; import BridgelessModule from './NativeRNIterableAPI'; From 6c3556ca8cbdf5c33f72d158ddfe31deec165bbd Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 1 Aug 2025 11:19:40 -0700 Subject: [PATCH 8/8] feat: added legacy support --- example/ios/Podfile | 2 +- .../project.pbxproj | 84 +++--- ios/RNIterableAPI/RNIterableAPI.h | 7 +- ios/RNIterableAPI/RNIterableAPI.mm | 273 +++++++++++++++++- 4 files changed, 313 insertions(+), 53 deletions(-) diff --git a/example/ios/Podfile b/example/ios/Podfile index 8b0cd0fb3..ecddcd509 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,4 +1,4 @@ -ENV['RCT_NEW_ARCH_ENABLED'] = '1' +ENV['RCT_NEW_ARCH_ENABLED'] = '0' # Resolve react_native_pods.rb with node to allow for hoisting require Pod::Executable.execute_command('node', ['-p', diff --git a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj index 14f2b3d55..d397e3e01 100644 --- a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj +++ b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj @@ -9,13 +9,13 @@ /* Begin PBXBuildFile section */ 00E356F31AD99517003FC87E /* ReactNativeSdkExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* ReactNativeSdkExampleTests.m */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; - 65AD4F06C28F65CB4E898F9A /* libPods-NotificationExt.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FA966B65909B06BFEB95FCC9 /* libPods-NotificationExt.a */; }; 779227342DFA3FB500D69EC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779227332DFA3FB500D69EC0 /* AppDelegate.swift */; }; 77DB813A2E330329004FEDA8 /* NotificationExt.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 77DB81332E330329004FEDA8 /* NotificationExt.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 77DB81432E33050B004FEDA8 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77DB81412E33050B004FEDA8 /* NotificationService.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; - 93D86B8B77018E3DCD97C1C8 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FE600D24FF78AE6CC769C16D /* libPods-ReactNativeSdkExample.a */; }; A3A40C20801B8F02005FA4C0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */; }; + BFAED3EB36E530C28BF8BC22 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 97D33EB1A903B7D4D2CB8319 /* libPods-ReactNativeSdkExample.a */; }; + E93759FF06B2B594995B2D8A /* libPods-NotificationExt.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 55EA585EF01B9EE9C9EA4954 /* libPods-NotificationExt.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -57,9 +57,10 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ReactNativeSdkExample/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ReactNativeSdkExample/Info.plist; sourceTree = ""; }; 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 146FE919EE16677339560C8B /* Pods-ReactNativeSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.debug.xcconfig"; sourceTree = ""; }; 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 65D78EC9700CB673F46D24E3 /* Pods-NotificationExt.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationExt.release.xcconfig"; path = "Target Support Files/Pods-NotificationExt/Pods-NotificationExt.release.xcconfig"; sourceTree = ""; }; - 6F7894ECC91E456C22D0D324 /* Pods-ReactNativeSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.debug.xcconfig"; sourceTree = ""; }; + 55EA585EF01B9EE9C9EA4954 /* libPods-NotificationExt.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NotificationExt.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5AD7E4C807981D45AFFE46B0 /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; 779227312DFA3FB500D69EC0 /* ReactNativeSdkExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExample-Bridging-Header.h"; sourceTree = ""; }; 779227322DFA3FB500D69EC0 /* ReactNativeSdkExampleTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExampleTests-Bridging-Header.h"; sourceTree = ""; }; 779227332DFA3FB500D69EC0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = ReactNativeSdkExample/AppDelegate.swift; sourceTree = ""; }; @@ -68,11 +69,10 @@ 77DB81402E33050B004FEDA8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 77DB81412E33050B004FEDA8 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReactNativeSdkExample/LaunchScreen.storyboard; sourceTree = ""; }; - A9DBF092882E5C0FB40D71CC /* Pods-NotificationExt.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationExt.debug.xcconfig"; path = "Target Support Files/Pods-NotificationExt/Pods-NotificationExt.debug.xcconfig"; sourceTree = ""; }; - A9DDB244D6B41D1A8EE903ED /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; + 9517CD9EA7E72B78695AEEF6 /* Pods-NotificationExt.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationExt.release.xcconfig"; path = "Target Support Files/Pods-NotificationExt/Pods-NotificationExt.release.xcconfig"; sourceTree = ""; }; + 97D33EB1A903B7D4D2CB8319 /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + B907B5B39032E170236A4FAE /* Pods-NotificationExt.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationExt.debug.xcconfig"; path = "Target Support Files/Pods-NotificationExt/Pods-NotificationExt.debug.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; - FA966B65909B06BFEB95FCC9 /* libPods-NotificationExt.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NotificationExt.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - FE600D24FF78AE6CC769C16D /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -87,7 +87,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 93D86B8B77018E3DCD97C1C8 /* libPods-ReactNativeSdkExample.a in Frameworks */, + BFAED3EB36E530C28BF8BC22 /* libPods-ReactNativeSdkExample.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -95,7 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 65AD4F06C28F65CB4E898F9A /* libPods-NotificationExt.a in Frameworks */, + E93759FF06B2B594995B2D8A /* libPods-NotificationExt.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -139,8 +139,8 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - FE600D24FF78AE6CC769C16D /* libPods-ReactNativeSdkExample.a */, - FA966B65909B06BFEB95FCC9 /* libPods-NotificationExt.a */, + 55EA585EF01B9EE9C9EA4954 /* libPods-NotificationExt.a */, + 97D33EB1A903B7D4D2CB8319 /* libPods-ReactNativeSdkExample.a */, ); name = Frameworks; sourceTree = ""; @@ -190,10 +190,10 @@ BBD78D7AC51CEA395F1C20DB /* Pods */ = { isa = PBXGroup; children = ( - 6F7894ECC91E456C22D0D324 /* Pods-ReactNativeSdkExample.debug.xcconfig */, - A9DDB244D6B41D1A8EE903ED /* Pods-ReactNativeSdkExample.release.xcconfig */, - A9DBF092882E5C0FB40D71CC /* Pods-NotificationExt.debug.xcconfig */, - 65D78EC9700CB673F46D24E3 /* Pods-NotificationExt.release.xcconfig */, + B907B5B39032E170236A4FAE /* Pods-NotificationExt.debug.xcconfig */, + 9517CD9EA7E72B78695AEEF6 /* Pods-NotificationExt.release.xcconfig */, + 146FE919EE16677339560C8B /* Pods-ReactNativeSdkExample.debug.xcconfig */, + 5AD7E4C807981D45AFFE46B0 /* Pods-ReactNativeSdkExample.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -223,14 +223,14 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ReactNativeSdkExample" */; buildPhases = ( - A613F9753095F1486E9898F7 /* [CP] Check Pods Manifest.lock */, + 20B797F7313F2F12DC05685C /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - 604F8A3ACFF9FF61E3A803F1 /* [CP] Embed Pods Frameworks */, - 2E4E0E4099ECD57D65389F08 /* [CP] Copy Pods Resources */, 77DB813F2E330329004FEDA8 /* Embed Foundation Extensions */, + FD3DA20D23B354970BCA7230 /* [CP] Embed Pods Frameworks */, + 125B666575EF47D7991C7194 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -246,7 +246,7 @@ isa = PBXNativeTarget; buildConfigurationList = 77DB813C2E330329004FEDA8 /* Build configuration list for PBXNativeTarget "NotificationExt" */; buildPhases = ( - 2E911B0C90954AA53C83E406 /* [CP] Check Pods Manifest.lock */, + F957C8DF4D8BEC9A3C70947C /* [CP] Check Pods Manifest.lock */, 77DB812F2E330329004FEDA8 /* Sources */, 77DB81302E330329004FEDA8 /* Frameworks */, 77DB81312E330329004FEDA8 /* Resources */, @@ -346,7 +346,7 @@ shellPath = /bin/sh; shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; }; - 2E4E0E4099ECD57D65389F08 /* [CP] Copy Pods Resources */ = { + 125B666575EF47D7991C7194 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -363,7 +363,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 2E911B0C90954AA53C83E406 /* [CP] Check Pods Manifest.lock */ = { + 20B797F7313F2F12DC05685C /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -378,50 +378,50 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-NotificationExt-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-ReactNativeSdkExample-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 604F8A3ACFF9FF61E3A803F1 /* [CP] Embed Pods Frameworks */ = { + F957C8DF4D8BEC9A3C70947C /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-NotificationExt-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - A613F9753095F1486E9898F7 /* [CP] Check Pods Manifest.lock */ = { + FD3DA20D23B354970BCA7230 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ReactNativeSdkExample-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -527,7 +527,7 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6F7894ECC91E456C22D0D324 /* Pods-ReactNativeSdkExample.debug.xcconfig */; + baseConfigurationReference = 146FE919EE16677339560C8B /* Pods-ReactNativeSdkExample.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -558,7 +558,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A9DDB244D6B41D1A8EE903ED /* Pods-ReactNativeSdkExample.release.xcconfig */; + baseConfigurationReference = 5AD7E4C807981D45AFFE46B0 /* Pods-ReactNativeSdkExample.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -587,7 +587,7 @@ }; 77DB813D2E330329004FEDA8 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A9DBF092882E5C0FB40D71CC /* Pods-NotificationExt.debug.xcconfig */; + baseConfigurationReference = B907B5B39032E170236A4FAE /* Pods-NotificationExt.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; @@ -629,7 +629,7 @@ }; 77DB813E2E330329004FEDA8 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 65D78EC9700CB673F46D24E3 /* Pods-NotificationExt.release.xcconfig */; + baseConfigurationReference = 9517CD9EA7E72B78695AEEF6 /* Pods-NotificationExt.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; diff --git a/ios/RNIterableAPI/RNIterableAPI.h b/ios/RNIterableAPI/RNIterableAPI.h index 71380823d..84c8c1d87 100644 --- a/ios/RNIterableAPI/RNIterableAPI.h +++ b/ios/RNIterableAPI/RNIterableAPI.h @@ -9,10 +9,9 @@ #import @interface RNIterableAPI : RCTEventEmitter -// #else -// #import -// #import -// @interface RNIterableAPI : RCTEventEmitter +#else +#import +@interface RNIterableAPI : RCTEventEmitter #endif diff --git a/ios/RNIterableAPI/RNIterableAPI.mm b/ios/RNIterableAPI/RNIterableAPI.mm index de1ac5346..a0dc695e5 100644 --- a/ios/RNIterableAPI/RNIterableAPI.mm +++ b/ios/RNIterableAPI/RNIterableAPI.mm @@ -1,5 +1,9 @@ #import "RNIterableAPI.h" -#import "RNIterableAPISpec.h" + +#if RCT_NEW_ARCH_ENABLED + #import "RNIterableAPISpec.h" +#endif + #import // umbrella (Objective-C) header // Forward-declare the Swift protocols/enum used in the Swift header. @@ -17,7 +21,6 @@ typedef NS_ENUM(NSInteger, InAppShowResponse) { @interface RNIterableAPI () @end - @implementation RNIterableAPI { ReactIterableAPI *_swiftAPI; } @@ -38,10 +41,16 @@ - (instancetype)init { // return [_swiftAPI supportedEvents]; // } + - (NSArray *)supportedEvents { return [ReactIterableAPI supportedEvents]; } +- (void)sendEventWithName:(NSString * _Nonnull)name result:(double)result { + [self sendEventWithName:name body:@(result)]; +} + +#if RCT_NEW_ARCH_ENABLED - (void)startObserving { NSLog(@"ReactNativeSdk startObserving"); [(ReactIterableAPI *)_swiftAPI startObserving]; @@ -57,10 +66,6 @@ - (void)hello { [(ReactIterableAPI *)_swiftAPI hello]; } -- (void)sendEventWithName:(NSString * _Nonnull)name result:(double)result { - [self sendEventWithName:name body:@(result)]; -} - - (void)testEventDispatch { NSLog(@"***ITBL OBJ-C*** testEventDispatch"); [_swiftAPI testEventDispatch]; @@ -341,5 +346,261 @@ - (void)passAlongAuthToken:(NSString *)authToken { return std::make_shared(params); } +#else + +RCT_EXPORT_METHOD(startObserving) { + NSLog(@"ReactNativeSdk startObserving"); + [(ReactIterableAPI *)_swiftAPI startObserving]; +} + +RCT_EXPORT_METHOD(stopObserving) { + NSLog(@"ReactNativeSdk stopObserving"); + [(ReactIterableAPI *)_swiftAPI stopObserving]; +} + +RCT_EXPORT_METHOD(hello) { + NSLog(@"***ITBL OBJ-C*** hello"); + [_swiftAPI hello]; +} + +RCT_EXPORT_METHOD(testEventDispatch) { + NSLog(@"***ITBL OBJ-C*** testEventDispatch"); + [_swiftAPI testEventDispatch]; +} + +RCT_EXPORT_METHOD(initializeWithApiKey:(NSString *)apiKey + config:(NSDictionary *)config + version:(NSString *)version + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSLog(@"ReactNativeSdk initializeWithApiKey"); + [_swiftAPI initializeWithApiKey:apiKey + config:config + version:version + resolver:resolve + rejecter:reject]; +} + +RCT_EXPORT_METHOD(initialize2WithApiKey:(NSString *)apiKey + config:(NSDictionary *)config + version:(NSString *)version + apiEndPointOverride:(NSString *)apiEndPointOverride + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSLog(@"ReactNativeSdk initialize2WithApiKey"); + [_swiftAPI initialize2WithApiKey:apiKey + config:config + apiEndPointOverride:apiEndPointOverride + version:version + resolver:resolve + rejecter:reject]; +} + +RCT_EXPORT_METHOD(setEmail:(NSString * _Nullable)email + authToken:(NSString * _Nullable)authToken) { + NSLog(@"ReactNativeSdk setEmail"); + [_swiftAPI setEmail:email authToken:authToken]; +} + +RCT_EXPORT_METHOD(getEmail:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSLog(@"ReactNativeSdk getEmail"); + [_swiftAPI getEmail:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(setUserId:(NSString * _Nullable)userId + authToken:(NSString * _Nullable)authToken) { + NSLog(@"ReactNativeSdk setUserId"); + [_swiftAPI setUserId:userId authToken:authToken]; +} + +RCT_EXPORT_METHOD(getUserId:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSLog(@"ReactNativeSdk getUserId"); + [_swiftAPI getUserId:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(setInAppShowResponse:(NSNumber *)inAppShowResponse) { + NSLog(@"ReactNativeSdk setInAppShowResponse"); + [_swiftAPI setInAppShowResponse:inAppShowResponse]; +} + +RCT_EXPORT_METHOD(getInAppMessages:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSLog(@"ReactNativeSdk getInAppMessages"); + [_swiftAPI getInAppMessages:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(getInboxMessages:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSLog(@"ReactNativeSdk getInboxMessages"); + [_swiftAPI getInboxMessages:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(getUnreadInboxMessagesCount:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSLog(@"ReactNativeSdk getUnreadInboxMessagesCount"); + [_swiftAPI getUnreadInboxMessagesCount:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(showMessage:(NSString *)messageId + consume:(BOOL)consume + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSLog(@"ReactNativeSdk showMessage"); + [_swiftAPI showMessage:messageId consume:consume resolver:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(removeMessage:(NSString *)messageId + location:(NSNumber *)location + source:(NSNumber *)source) { + NSLog(@"ReactNativeSdk removeMessage"); + [_swiftAPI removeMessage:messageId location:location source:source]; +} + +RCT_EXPORT_METHOD(setReadForMessage:(NSString *)messageId + read:(BOOL)read) { + NSLog(@"ReactNativeSdk setReadForMessage"); + [_swiftAPI setReadForMessage:messageId read:read]; +} + +RCT_EXPORT_METHOD(setAutoDisplayPaused:(BOOL)autoDisplayPaused) { + NSLog(@"ReactNativeSdk setAutoDisplayPaused"); + [_swiftAPI setAutoDisplayPaused:autoDisplayPaused]; +} + +RCT_EXPORT_METHOD(trackEvent:(NSString *)name + dataFields:(NSDictionary *)dataFields) { + NSLog(@"ReactNativeSdk trackEvent"); + [_swiftAPI trackEvent:name dataFields:dataFields]; +} + +RCT_EXPORT_METHOD(trackPushOpenWithCampaignId:(NSNumber *)campaignId + templateId:(NSNumber *)templateId + messageId:(NSString *)messageId + appAlreadyRunning:(BOOL)appAlreadyRunning + dataFields:(NSDictionary *)dataFields) { + NSLog(@"ReactNativeSdk trackPushOpenWithCampaignId"); + [_swiftAPI trackPushOpenWithCampaignId:campaignId templateId:templateId messageId:messageId appAlreadyRunning:appAlreadyRunning dataFields:dataFields]; +} + +RCT_EXPORT_METHOD(trackInAppOpen:(NSString *)messageId + location:(NSNumber *)location) { + NSLog(@"ReactNativeSdk trackInAppOpen"); + [_swiftAPI trackInAppOpen:messageId location:location]; +} + +RCT_EXPORT_METHOD(trackInAppClick:(NSString *)messageId + location:(NSNumber *)location + clickedUrl:(NSString *)clickedUrl) { + NSLog(@"ReactNativeSdk trackInAppClick"); + [_swiftAPI trackInAppClick:messageId location:location clickedUrl:clickedUrl]; +} + +RCT_EXPORT_METHOD(trackInAppClose:(NSString *)messageId + location:(NSNumber *)location + source:(NSNumber *)source + clickedUrl:(NSString *)clickedUrl) { + NSLog(@"ReactNativeSdk trackInAppClose"); + [_swiftAPI trackInAppClose:messageId location:location source:source clickedUrl:clickedUrl]; +} + +RCT_EXPORT_METHOD(inAppConsume:(NSString *)messageId + location:(NSNumber *)location + source:(NSNumber *)source) { + NSLog(@"ReactNativeSdk inAppConsume"); + [_swiftAPI inAppConsume:messageId location:location source:source]; +} + +RCT_EXPORT_METHOD(updateCart:(NSArray *)items) { + NSLog(@"ReactNativeSdk updateCart"); + [_swiftAPI updateCart:items]; +} + +RCT_EXPORT_METHOD(trackPurchase:(NSNumber *)total + items:(NSArray *)items + dataFields:(NSDictionary *)dataFields) { + NSLog(@"ReactNativeSdk trackPurchase"); + [_swiftAPI trackPurchase:total items:items dataFields:dataFields]; +} + +RCT_EXPORT_METHOD(updateUser:(NSDictionary *)dataFields + mergeNestedObjects:(BOOL)mergeNestedObjects) { + NSLog(@"ReactNativeSdk updateUser"); + [_swiftAPI updateUser:dataFields mergeNestedObjects:mergeNestedObjects]; +} + RCT_EXPORT_METHOD(updateEmail:(NSString *)email + authToken:(NSString *)authToken) { + NSLog(@"ReactNativeSdk updateEmail"); + [_swiftAPI updateEmail:email authToken:authToken]; +} + +RCT_EXPORT_METHOD(getAttributionInfo:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSLog(@"ReactNativeSdk getAttributionInfo"); + [_swiftAPI getAttributionInfo:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(setAttributionInfo:(NSDictionary *)attributionInfo) { + NSLog(@"ReactNativeSdk setAttributionInfo"); + [_swiftAPI setAttributionInfo:attributionInfo]; +} + +RCT_EXPORT_METHOD(disableDeviceForCurrentUser) { + NSLog(@"ReactNativeSdk disableDeviceForCurrentUser"); + [_swiftAPI disableDeviceForCurrentUser]; +} + +RCT_EXPORT_METHOD(getLastPushPayload:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSLog(@"ReactNativeSdk getLastPushPayload"); + [_swiftAPI getLastPushPayload:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(getHtmlInAppContentForMessage:(NSString *)messageId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSLog(@"ReactNativeSdk getHtmlInAppContentForMessage"); + [_swiftAPI getHtmlInAppContentForMessage:messageId resolver:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(handleAppLink:(NSString *)appLink + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSLog(@"ReactNativeSdk handleAppLink"); + [_swiftAPI handleAppLink:appLink resolver:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(updateSubscriptions:(NSArray *)emailListIds + unsubscribedChannelIds:(NSArray *)unsubscribedChannelIds + unsubscribedMessageTypeIds:(NSArray *)unsubscribedMessageTypeIds + subscribedMessageTypeIds:(NSArray *)subscribedMessageTypeIds + campaignId:(NSNumber *)campaignId + templateId:(NSNumber *)templateId) { + NSLog(@"ReactNativeSdk updateSubscriptions"); + [_swiftAPI updateSubscriptions:emailListIds unsubscribedChannelIds:unsubscribedChannelIds unsubscribedMessageTypeIds:unsubscribedMessageTypeIds subscribedMessageTypeIds:subscribedMessageTypeIds campaignId:campaignId templateId:templateId]; +} + +RCT_EXPORT_METHOD(startSession:(NSArray *)visibleRows) { + NSLog(@"ReactNativeSdk startSession"); + [_swiftAPI startSession:visibleRows]; +} + +RCT_EXPORT_METHOD(endSession) { + NSLog(@"ReactNativeSdk endSession"); + [_swiftAPI endSession]; +} + +RCT_EXPORT_METHOD(updateVisibleRows:(NSArray *)visibleRows) { + NSLog(@"ReactNativeSdk updateVisibleRows"); + [_swiftAPI updateVisibleRows:visibleRows]; +} + +RCT_EXPORT_METHOD(passAlongAuthToken:(NSString *)authToken) { + NSLog(@"ReactNativeSdk passAlongAuthToken"); + [_swiftAPI passAlongAuthToken:authToken]; +} + +#endif @end