From 9859dc43f1824be62d1d13d71a2b63ad91e0d4b6 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 3 Jun 2025 12:45:08 -0700 Subject: [PATCH 01/14] feat: add support for new architecture and Fabric in Android and iOS SDKs --- Iterable-React-Native-SDK.podspec | 5 +++-- android/build.gradle | 30 ++++++++++++++++++++++++++++++ android/gradle.properties | 6 +++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/Iterable-React-Native-SDK.podspec b/Iterable-React-Native-SDK.podspec index 14c6d5966..eb342e8a5 100644 --- a/Iterable-React-Native-SDK.podspec +++ b/Iterable-React-Native-SDK.podspec @@ -16,6 +16,8 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,swift}" + s.dependency "Iterable-iOS-SDK", "6.5.4" + # 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) @@ -36,9 +38,8 @@ Pod::Spec.new do |s| s.dependency "RCTRequired" s.dependency "RCTTypeSafety" s.dependency "ReactCommon/turbomodule/core" + s.dependency "React-RCTFabric" # This is for Fabric support end end - s.dependency "Iterable-iOS-SDK", "6.5.4" - end diff --git a/android/build.gradle b/android/build.gradle index fd26bc186..6c7e66943 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -23,6 +23,10 @@ def isNewArchitectureEnabled() { return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" } +def isFabricEnabled() { + return rootProject.hasProperty("fabricEnabled") && rootProject.getProperty("fabricEnabled") == "true" +} + apply plugin: "com.android.library" apply plugin: "kotlin-android" @@ -64,6 +68,17 @@ android { minSdkVersion getExtOrIntegerDefault("minSdkVersion") targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + if (isNewArchitectureEnabled()) { + buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true") + if (isFabricEnabled()) { + buildConfigField("boolean", "IS_FABRIC_ENABLED", "true") + } else { + buildConfigField("boolean", "IS_FABRIC_ENABLED", "false") + } + } else { + buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", "false") + buildConfigField("boolean", "IS_FABRIC_ENABLED", "false") + } } buildTypes { @@ -80,6 +95,10 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + buildFeatures { + buildConfig true + } } repositories { @@ -97,5 +116,16 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" api "com.iterable:iterableapi:3.5.2" // api project(":iterableapi") // links to local android SDK repo rather than by release + + if (isNewArchitectureEnabled()) { + implementation project(":ReactAndroid") + implementation project(":ReactCommon") + implementation project(":ReactCommon:jsi") + implementation project(":ReactCommon:reactnativejni") + implementation project(":ReactCommon:reactnativejni:jsi") + implementation project(":ReactCommon:reactnativejni:jsc") + implementation project(":ReactCommon:reactnativejni:hermes") + implementation project(":ReactCommon:reactnativejni:fabricjni") + } } diff --git a/android/gradle.properties b/android/gradle.properties index a46c61ab2..a547098bf 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -4,4 +4,8 @@ RNIterable_targetSdkVersion=31 RNIterable_compileSdkVersion=31 RNIterable_ndkversion=21.4.7075529 android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true + +# New Architecture flags +newArchEnabled=false +fabricEnabled=false From bc0f5a1b0a92d60b028fd7ae8f60940918b0ca1e Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 4 Jun 2025 12:30:09 -0700 Subject: [PATCH 02/14] feat: enable new architecture and Fabric support in Android and iOS SDKs --- android/build.gradle | 13 +--- android/gradle.properties | 4 - example/android/gradle.properties | 20 +++-- example/ios/Podfile | 9 ++- example/ios/Podfile.lock | 117 ++++++++++++++++++++++++++---- example/react-native.config.js | 6 ++ 6 files changed, 130 insertions(+), 39 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 6c7e66943..71a0639b5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -115,17 +115,6 @@ dependencies { implementation "com.facebook.react:react-native:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" api "com.iterable:iterableapi:3.5.2" - // api project(":iterableapi") // links to local android SDK repo rather than by release - - if (isNewArchitectureEnabled()) { - implementation project(":ReactAndroid") - implementation project(":ReactCommon") - implementation project(":ReactCommon:jsi") - implementation project(":ReactCommon:reactnativejni") - implementation project(":ReactCommon:reactnativejni:jsi") - implementation project(":ReactCommon:reactnativejni:jsc") - implementation project(":ReactCommon:reactnativejni:hermes") - implementation project(":ReactCommon:reactnativejni:fabricjni") - } + // api project(":iterableapi") // links to local android SDK repo rather than by release } diff --git a/android/gradle.properties b/android/gradle.properties index a547098bf..9a7463450 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -5,7 +5,3 @@ RNIterable_compileSdkVersion=31 RNIterable_ndkversion=21.4.7075529 android.useAndroidX=true android.enableJetifier=true - -# New Architecture flags -newArchEnabled=false -fabricEnabled=false diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 289375fe5..b10f2c5fb 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -27,12 +27,7 @@ android.useAndroidX=true # ./gradlew -PreactNativeArchitectures=x86_64 reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 -# Use this property to enable support to the new architecture. -# This will allow you to use TurboModules and the Fabric render in -# your application. You should enable this flag either if you want -# to write custom TurboModules/Fabric components OR use libraries that -# are providing them. -newArchEnabled=false + # Use this property to enable or disable the Hermes JS engine. # If set to false, you will be using JSC instead. @@ -40,4 +35,15 @@ hermesEnabled=true # Needed for react-native-webview # See: https://github.com/react-native-webview/react-native-webview/blob/HEAD/docs/Getting-Started.md -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true + +######################################################## +# New Architecture flags +######################################################## +# Use this property to enable support to the new architecture. +# This will allow you to use TurboModules and the Fabric render in +# your application. You should enable this flag either if you want +# to write custom TurboModules/Fabric components OR use libraries that +# are providing them. +newArchEnabled=true +fabricEnabled=true diff --git a/example/ios/Podfile b/example/ios/Podfile index 1b747c965..e0fcf5ba0 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -8,6 +8,9 @@ require Pod::Executable.execute_command('node', ['-p', platform :ios, min_ios_version_supported prepare_react_native_project! +# Enable new architecture +ENV['RCT_NEW_ARCH_ENABLED'] = '1' + linkage = ENV['USE_FRAMEWORKS'] if linkage != nil Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green @@ -20,7 +23,11 @@ target 'ReactNativeSdkExample' do use_react_native!( :path => config[:reactNativePath], # An absolute path to your application root. - :app_path => "#{Pod::Config.instance.installation_root}/.." + :app_path => "#{Pod::Config.instance.installation_root}/..", + # Enable new architecture + :fabric_enabled => true, + # Enable Hermes + :hermes_enabled => true ) target 'ReactNativeSdkExampleTests' do diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 66f2bd0cb..85a46af4f 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -8,7 +8,7 @@ PODS: - hermes-engine/Pre-built (= 0.75.3) - hermes-engine/Pre-built (0.75.3) - Iterable-iOS-SDK (6.5.4) - - Iterable-React-Native-SDK (2.0.0-alpha): + - Iterable-React-Native-SDK (2.0.0-beta.1): - DoubleConversion - glog - hermes-engine @@ -1261,7 +1261,71 @@ PODS: - ReactCommon/turbomodule/core - Yoga - react-native-safe-area-context (4.11.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - react-native-safe-area-context/common (= 4.11.1) + - react-native-safe-area-context/fabric (= 4.11.1) + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context/common (4.11.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context/fabric (4.11.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - react-native-safe-area-context/common + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-webview (13.12.3): - DoubleConversion - glog @@ -1565,6 +1629,29 @@ PODS: - ReactCommon/turbomodule/core - Yoga - RNScreens (3.34.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNScreens/common (= 3.34.0) + - Yoga + - RNScreens/common (3.34.0): - DoubleConversion - glog - hermes-engine @@ -1834,7 +1921,7 @@ SPEC CHECKSUMS: glog: 69ef571f3de08433d766d614c73a9838a06bf7eb hermes-engine: 8d2103d6c0176779aea4e25df6bb1410f9946680 Iterable-iOS-SDK: 710bf6dada7dae9c03a09db7f0bc6c9d9a671f40 - Iterable-React-Native-SDK: 704ac7fdc74f9a60621537f7724fe352f9d558c6 + Iterable-React-Native-SDK: f7d3f5792998368806a78c92143f20418ce2ddeb RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740 RCTDeprecation: 4191f6e64b72d9743f6fe1a8a16e89e868f5e9e7 RCTRequired: 9bb589570f2bb3abc6518761e3fd1ad9b7f7f06c @@ -1845,16 +1932,16 @@ SPEC CHECKSUMS: React-CoreModules: 2d68c251bc4080028f2835fa47504e8f20669a21 React-cxxreact: 5f233f8ac7ea4772e49462e0ab2b0a15a4f80ab7 React-debug: fd0ed8ecd5f8a23c7daf5ceaca8aa722a4d083fd - React-defaultsnativemodule: 10f0f8bc38d8dc7d2273572cd85ed0b71298ecdd - React-domnativemodule: bfef3dda59e7030b498d0d78628f4adf414ab8e4 + React-defaultsnativemodule: 1f9a0cae1ef7e05a6ea2bbec2e5eff6eb70da16a + React-domnativemodule: 38d632c6963ab2d08f5ce67808e070439bd1461c React-Fabric: 3d0f5e2735d2f77a897ee684edeff7bb0e061919 React-FabricComponents: 68032a85a3c25c9c8d6ce676d8af9a85e2370f24 React-FabricImage: f8ac2df576703097b5b2f8d972b162cdca855aa3 React-featureflags: cf78861db9318ae29982fa8953c92d31b276c9ac - React-featureflagsnativemodule: d04eb5c3f0ac33fe70b060d97e8649bfd69c5f1e + React-featureflagsnativemodule: 99ffda7fc2cc0f9578b05b84d8b4a2e9dcb39b8b React-graphics: 7572851bca7242416b648c45d6af87d93d29281e React-hermes: 95c27801c60615345ee6256eafa6d597ce983b8b - React-idlecallbacksnativemodule: f5f0b760ec2739b30e315e1afee3dd3a5a93c3b6 + React-idlecallbacksnativemodule: e4fd9ee09b8481dd22f1e173984e5ee2730712ce React-ImageManager: aedf54d34d4475c66f4c3da6b8359b95bee904e4 React-jserrorhandler: 0c8949672a00f2a502c767350e591e3ec3d82fb3 React-jsi: d77bb442a4b0849063f2bd22d3c1fa71918713b7 @@ -1863,18 +1950,18 @@ SPEC CHECKSUMS: React-jsitracing: 3935b092f85bb1e53b8cf8a00f572413648af46b React-logger: 4072f39df335ca443932e0ccece41fbeb5ca8404 React-Mapbuffer: 714f2fae68edcabfc332b754e9fbaa8cfc68fdd4 - React-microtasksnativemodule: 4943ad8f99be8ccf5a63329fa7d269816609df9e - react-native-safe-area-context: 5141f11858b033636f1788b14f32eaba92cee810 - react-native-webview: 926d2665cf3196e39c4449a72d136d0a53b9df8a + React-microtasksnativemodule: 0b6b90da7f203e3015e1252ec3cba49c8ddd85ad + react-native-safe-area-context: 2279fe040bc93af8624f7d034806180fdbe5fa02 + react-native-webview: 8039cbb0aa6ad2fd7918e23aebb5586f127d3e91 React-nativeconfig: 4a9543185905fe41014c06776bf126083795aed9 React-NativeModulesApple: 0506da59fc40d2e1e6e12a233db5e81c46face27 React-perflogger: 3bbb82f18e9ac29a1a6931568e99d6305ef4403b React-performancetimeline: d15a723422ed500f47cb271f3175abbeb217f5ba React-RCTActionSheet: cb2b38a53d03ec22f1159c89667b86c2c490d92d React-RCTAnimation: 6836c87c7364f471e9077fda80b7349bc674be33 - React-RCTAppDelegate: 2f11edfa7302451c792591f9a7838ca86cdcec34 + React-RCTAppDelegate: 603240f6a7d7eefeeffe4e29dd0be70dc35208cf React-RCTBlob: 516dbbd38397f5013394fdd1cc65408cc82e37a1 - React-RCTFabric: b281a52c2b9726b0c64880e1535f2100013d5f7c + React-RCTFabric: 7298604d497db4fe445cd704bd1097636643ee89 React-RCTImage: 1b2c2c1716db859ffff2d7a06a30b0ec5c677fc5 React-RCTLinking: 59c07577767e705b0ab95d11e5ad74c61bf2a022 React-RCTNetwork: f9a827e7d6bc428e0d99cd1fbe0427854354b8c1 @@ -1892,12 +1979,12 @@ SPEC CHECKSUMS: React-utils: f2afa6acd905ca2ce7bb8ffb4a22f7f8a12534e8 ReactCodegen: ff95a93d5ab5d9b2551571886271478eaa168565 ReactCommon: 289214026502e6a93484f4a46bcc0efa4f3f2864 - RNGestureHandler: 1b7c5ed637e02f6898d08bffb701d378be5e091d - RNScreens: 19719a9c326e925498ac3b2d35c4e50fe87afc06 - RNVectorIcons: 6382277afab3c54658e9d555ee0faa7a37827136 + RNGestureHandler: 364e6862a112045bb5c5d35601f0bdb0304af979 + RNScreens: de6e57426ba0e6cbc3fb5b4f496e7f08cb2773c2 + RNVectorIcons: 07792a9538e8577c1263fcad187712e90d65d8fb SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 4ef80d96a5534f0e01b3055f17d1e19a9fc61b63 -PODFILE CHECKSUM: aab4a30773612c4ffb73be13f5b169b8b156f374 +PODFILE CHECKSUM: 4c6a6a09af0b3466691c09d9d9af2515b974ddc9 COCOAPODS: 1.16.2 diff --git a/example/react-native.config.js b/example/react-native.config.js index 0b9606d4a..59d969820 100644 --- a/example/react-native.config.js +++ b/example/react-native.config.js @@ -10,6 +10,12 @@ module.exports = { dependencies: { [pkg.name]: { root: path.join(__dirname, '..'), + platforms: { + // Codegen script incorrectly fails without this + // So we explicitly specify the platforms with empty object + ios: {}, + android: {}, + }, }, }, }; From e6ad069dbc04c330a80d95337cf6b81763f76c19 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 4 Jun 2025 12:30:36 -0700 Subject: [PATCH 03/14] feat: update react-native-gesture-handler to 2.25.0 and adjust packaging options in Android build --- example/Gemfile.lock | 124 ++++++++++++++++++++++++++++++ example/android/app/build.gradle | 34 ++++++++ example/android/gradle.properties | 2 + example/ios/Podfile.lock | 98 +++++++++++------------ example/package.json | 2 +- yarn.lock | 11 ++- 6 files changed, 215 insertions(+), 56 deletions(-) create mode 100644 example/Gemfile.lock diff --git a/example/Gemfile.lock b/example/Gemfile.lock new file mode 100644 index 000000000..1b4d501e5 --- /dev/null +++ b/example/Gemfile.lock @@ -0,0 +1,124 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + activesupport (7.2.2.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + atomos (0.1.3) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.2.0) + claide (1.1.0) + cocoapods (1.15.2) + addressable (~> 2.8) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.15.2) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.8.0) + nap (~> 1.0) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.23.0, < 2.0) + cocoapods-core (1.15.2) + activesupport (>= 5.0, < 8) + addressable (~> 2.8) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + public_suffix (~> 4.0) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (2.1) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored2 (3.1.2) + concurrent-ruby (1.3.3) + connection_pool (2.5.3) + drb (2.2.3) + escape (0.0.4) + ethon (0.16.0) + ffi (>= 1.15.0) + ffi (1.17.2) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + httpclient (2.9.0) + mutex_m + i18n (1.14.7) + concurrent-ruby (~> 1.0) + json (2.12.2) + logger (1.7.0) + minitest (5.25.5) + molinillo (0.8.0) + mutex_m (0.3.0) + nanaimo (0.3.0) + nap (1.1.0) + netrc (0.11.0) + nkf (0.2.0) + public_suffix (4.0.7) + rexml (3.4.1) + ruby-macho (2.5.1) + securerandom (0.4.1) + typhoeus (1.4.1) + ethon (>= 0.9.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + xcodeproj (1.25.1) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (>= 3.3.6, < 4.0) + +PLATFORMS + ruby + +DEPENDENCIES + activesupport (>= 6.1.7.5, != 7.1.0) + benchmark + bigdecimal + cocoapods (>= 1.13, != 1.15.1, != 1.15.0) + concurrent-ruby (< 1.3.4) + logger + mutex_m + xcodeproj (< 1.26.0) + +RUBY VERSION + ruby 3.3.6p108 + +BUNDLED WITH + 2.5.23 diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 373c949fd..76962162d 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -85,6 +85,40 @@ android { versionCode 1 versionName "1.0" } + + packagingOptions { + exclude '**/libc++_shared.so' + exclude '**/libfbjni.so' + exclude '**/libfolly_runtime.so' + exclude '**/libglog.so' + exclude '**/libhermes.so' + exclude '**/libjsi.so' + exclude '**/libreact_codegen_rngesturehandler_codegen.so' + exclude '**/libreact_debug.so' + exclude '**/libreact_nativemodule_core.so' + exclude '**/libreact_render_debug.so' + exclude '**/libreact_render_graphics.so' + exclude '**/librrc_view.so' + exclude '**/libruntimeexecutor.so' + exclude '**/libturbomodulejsijni.so' + exclude '**/libyoga.so' + pickFirst '**/libc++_shared.so' + pickFirst '**/libfbjni.so' + pickFirst '**/libfolly_runtime.so' + pickFirst '**/libglog.so' + pickFirst '**/libhermes.so' + pickFirst '**/libjsi.so' + pickFirst '**/libreact_codegen_rngesturehandler_codegen.so' + pickFirst '**/libreact_debug.so' + pickFirst '**/libreact_nativemodule_core.so' + pickFirst '**/libreact_render_debug.so' + pickFirst '**/libreact_render_graphics.so' + pickFirst '**/librrc_view.so' + pickFirst '**/libruntimeexecutor.so' + pickFirst '**/libturbomodulejsijni.so' + pickFirst '**/libyoga.so' + } + signingConfigs { debug { storeFile file('debug.keystore') diff --git a/example/android/gradle.properties b/example/android/gradle.properties index b10f2c5fb..4af51c485 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -47,3 +47,5 @@ android.enableJetifier=true # are providing them. newArchEnabled=true fabricEnabled=true + +kotlin.compiler.allWarningsAsErrors=false diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 85a46af4f..366ccc4d8 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1607,7 +1607,7 @@ PODS: - React-logger (= 0.75.3) - React-perflogger (= 0.75.3) - React-utils (= 0.75.3) - - RNGestureHandler (2.20.1): + - RNGestureHandler (2.25.0): - DoubleConversion - glog - hermes-engine @@ -1921,70 +1921,70 @@ SPEC CHECKSUMS: glog: 69ef571f3de08433d766d614c73a9838a06bf7eb hermes-engine: 8d2103d6c0176779aea4e25df6bb1410f9946680 Iterable-iOS-SDK: 710bf6dada7dae9c03a09db7f0bc6c9d9a671f40 - Iterable-React-Native-SDK: f7d3f5792998368806a78c92143f20418ce2ddeb + Iterable-React-Native-SDK: 3f7f37ca61709b01328990dc7be823c43df52bd3 RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740 RCTDeprecation: 4191f6e64b72d9743f6fe1a8a16e89e868f5e9e7 RCTRequired: 9bb589570f2bb3abc6518761e3fd1ad9b7f7f06c RCTTypeSafety: 1c1a8741c86df0a0ac1a99cf3fb0e29eedbc2c88 React: b6810a201ee11e69ae8bfd4eb4aaab86610600bf React-callinvoker: d6c7898b63e6a2d37bc308f17c05be0ba3630b10 - React-Core: 2fc97900b68e7568233698c6113ca9d64ed8b520 - React-CoreModules: 2d68c251bc4080028f2835fa47504e8f20669a21 - React-cxxreact: 5f233f8ac7ea4772e49462e0ab2b0a15a4f80ab7 + React-Core: 411ef2293ba0c9147e04ee89324bc1575b38a089 + React-CoreModules: 30c44229d249317498dac4a984925c56e06f61c2 + React-cxxreact: 1ba92740ea3ed5be86898dec22f6548aa843da16 React-debug: fd0ed8ecd5f8a23c7daf5ceaca8aa722a4d083fd - React-defaultsnativemodule: 1f9a0cae1ef7e05a6ea2bbec2e5eff6eb70da16a - React-domnativemodule: 38d632c6963ab2d08f5ce67808e070439bd1461c - React-Fabric: 3d0f5e2735d2f77a897ee684edeff7bb0e061919 - React-FabricComponents: 68032a85a3c25c9c8d6ce676d8af9a85e2370f24 - React-FabricImage: f8ac2df576703097b5b2f8d972b162cdca855aa3 + React-defaultsnativemodule: b013b8fc528e0830c0397250bd565e54c6eb3b90 + React-domnativemodule: 1e0a9cdd989be6905ff5867ab4b060e3be3900e4 + React-Fabric: da5caca65022dcbbb95d12cebcde7770fdd44ff0 + React-FabricComponents: 19e0eb8be8d8f2afa1a921705a87d8818eba14cf + React-FabricImage: 50df9e6aace1781cf23e130171631d7674b4072c React-featureflags: cf78861db9318ae29982fa8953c92d31b276c9ac - React-featureflagsnativemodule: 99ffda7fc2cc0f9578b05b84d8b4a2e9dcb39b8b - React-graphics: 7572851bca7242416b648c45d6af87d93d29281e - React-hermes: 95c27801c60615345ee6256eafa6d597ce983b8b - React-idlecallbacksnativemodule: e4fd9ee09b8481dd22f1e173984e5ee2730712ce - React-ImageManager: aedf54d34d4475c66f4c3da6b8359b95bee904e4 - React-jserrorhandler: 0c8949672a00f2a502c767350e591e3ec3d82fb3 - React-jsi: d77bb442a4b0849063f2bd22d3c1fa71918713b7 - React-jsiexecutor: 3b9c6334b7b0f42d4c4aae950132766e63a7809f - React-jsinspector: e1bb5816869507527c30213cc1ed60eae9e3e9c4 - React-jsitracing: 3935b092f85bb1e53b8cf8a00f572413648af46b - React-logger: 4072f39df335ca443932e0ccece41fbeb5ca8404 - React-Mapbuffer: 714f2fae68edcabfc332b754e9fbaa8cfc68fdd4 - React-microtasksnativemodule: 0b6b90da7f203e3015e1252ec3cba49c8ddd85ad - react-native-safe-area-context: 2279fe040bc93af8624f7d034806180fdbe5fa02 - react-native-webview: 8039cbb0aa6ad2fd7918e23aebb5586f127d3e91 + React-featureflagsnativemodule: 8a6f72b439f2f48d80d0d52f4a2d30769fdd4727 + React-graphics: 7ed2dc99f706228448b870882729a8303343b5a5 + React-hermes: 167b427c2106b92ac47add9b35ca024d42453518 + React-idlecallbacksnativemodule: 26d5b4ce0389063f9659e8ebe6a14f87e7affdfb + React-ImageManager: 9970421c57b6458d3a4d6ce319c9067217c4882f + React-jserrorhandler: 6764a4b7abd617332fb0935c9ba63a6369207a15 + React-jsi: 7713fae6d70c49a1b1b12d7e65ca62a50cd820d2 + React-jsiexecutor: 67260e3eb3d1f3d3fd41ff15e89ce4027ae9c36a + React-jsinspector: a0f1febb0bcf5770ff135444a6afee7520ee42f7 + React-jsitracing: bf77e00063522e4fd6d84fa129f0caaf360d275e + React-logger: 7e56c9eceafd7f45e98c16cb42ff3c9966c67119 + React-Mapbuffer: e68dd904f0f3a84dd35989288ed3bcf5e37f9737 + React-microtasksnativemodule: 2b22a88c8abe1a90aca1804e55717190d3a26cee + react-native-safe-area-context: 17800bbaacdd18289d68ac8d4eb541c1a183463f + react-native-webview: 4cfaa2237960fb97ad5de782cf97ea475bf09af4 React-nativeconfig: 4a9543185905fe41014c06776bf126083795aed9 - React-NativeModulesApple: 0506da59fc40d2e1e6e12a233db5e81c46face27 + React-NativeModulesApple: f6b6dc0998c945dd113858f1fc12e5e5f0da0990 React-perflogger: 3bbb82f18e9ac29a1a6931568e99d6305ef4403b - React-performancetimeline: d15a723422ed500f47cb271f3175abbeb217f5ba + React-performancetimeline: 05c0372923c2f3a9e8a5ae954258f0436003bffb React-RCTActionSheet: cb2b38a53d03ec22f1159c89667b86c2c490d92d - React-RCTAnimation: 6836c87c7364f471e9077fda80b7349bc674be33 - React-RCTAppDelegate: 603240f6a7d7eefeeffe4e29dd0be70dc35208cf - React-RCTBlob: 516dbbd38397f5013394fdd1cc65408cc82e37a1 - React-RCTFabric: 7298604d497db4fe445cd704bd1097636643ee89 - React-RCTImage: 1b2c2c1716db859ffff2d7a06a30b0ec5c677fc5 - React-RCTLinking: 59c07577767e705b0ab95d11e5ad74c61bf2a022 - React-RCTNetwork: f9a827e7d6bc428e0d99cd1fbe0427854354b8c1 - React-RCTSettings: 614252fecc24840f61590c016aca1664a52cfb0f - React-RCTText: 424549f68867265aa25969f50e7b9bf8bd70ae55 - React-RCTVibration: c8d156e6cce18f00b0310db7670fa997c7cda407 + React-RCTAnimation: c8be4f58eabb487d6346247ee8e7bac434737ed7 + React-RCTAppDelegate: 357634ff6819214e37538458053213fccc940967 + React-RCTBlob: 7a64271f64a60390a2e73edecaca2735be8044ff + React-RCTFabric: dcdea7c1ff2fa7ec2875a855f8e6ce228950884d + React-RCTImage: 4fb571875362a78ccc01aded76b94a71ae466b8b + React-RCTLinking: e825182eaf7f4047f6bb11bb6cd2ae5858008e66 + React-RCTNetwork: 0e07b83395b6ff5016f7cea4ac99426a893a1438 + React-RCTSettings: bd68792732f116994e992cf48e5bb70c4eb3910e + React-RCTText: c3cfce62ddb887cdd86403a6130a58a1f8fed9f3 + React-RCTVibration: 32a10228b7affa8de6401dba6f0d73b5a8433342 React-rendererconsistency: 993f54bb0df644df2922cd87ea55238d510d992b - React-rendererdebug: 7a8cbb632b68d666ad0fc01b3f9dc1a1bcc9a9f9 + React-rendererdebug: 9cd1f3e6d12c1d9b99fce6ceb373495b29b3d9ee React-rncore: 1df26fe0ae861c599f9f2896f45e8834ef4b85f9 - React-RuntimeApple: b5b14b09e3be4058f9fe7ab4925e1ee343f03310 - React-RuntimeCore: 2073fb33da2aec6ce6c1c9d3d53898ed1f1d806d + React-RuntimeApple: 5fb9053ae46ec14407f24547afd903ec8f0c0b9a + React-RuntimeCore: f6af8417106c1ce5b494edd99e86b0d0069ad6c3 React-runtimeexecutor: 9a668b94ad5d93755443311715bd57680330286a - React-RuntimeHermes: b37c62718d6920ac2958a0052bdc1b01aca842b8 - React-runtimescheduler: e25750a18cbb7469e0513f1ace834d14e8c1a202 - React-utils: f2afa6acd905ca2ce7bb8ffb4a22f7f8a12534e8 - ReactCodegen: ff95a93d5ab5d9b2551571886271478eaa168565 - ReactCommon: 289214026502e6a93484f4a46bcc0efa4f3f2864 - RNGestureHandler: 364e6862a112045bb5c5d35601f0bdb0304af979 - RNScreens: de6e57426ba0e6cbc3fb5b4f496e7f08cb2773c2 - RNVectorIcons: 07792a9538e8577c1263fcad187712e90d65d8fb + React-RuntimeHermes: 6c6053fb5aca5558b071cfbce2868ca50a3b8fc7 + React-runtimescheduler: 5d1a32712d441c38e6d5815069e1810d38ed26f7 + React-utils: 3c815e7d3abb801930a3df2db870c92855429fea + ReactCodegen: 3d11bcf0cac47a77042a3476a1c2f7058bfd6880 + ReactCommon: c65f7049a542669dcc7bff6b7a8071a039c7d0dd + RNGestureHandler: 8dab066435239ff9f56934a2dae9080734cfec2b + RNScreens: b941e3ee5fe1991bd25098b5fc8d72db88678793 + RNVectorIcons: a24016b773380b1aa37fca501ec6b94a951890a0 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 4ef80d96a5534f0e01b3055f17d1e19a9fc61b63 PODFILE CHECKSUM: 4c6a6a09af0b3466691c09d9d9af2515b974ddc9 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/example/package.json b/example/package.json index 4b74b0703..d25ddb9d0 100644 --- a/example/package.json +++ b/example/package.json @@ -16,7 +16,7 @@ "@react-navigation/stack": "^6.4.1", "react": "18.3.1", "react-native": "0.75.3", - "react-native-gesture-handler": "^2.20.1", + "react-native-gesture-handler": "^2.25.0", "react-native-safe-area-context": "^4.11.1", "react-native-screens": "^3.34.0", "react-native-vector-icons": "^10.2.0", diff --git a/yarn.lock b/yarn.lock index cf30f91fc..a024be031 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3058,7 +3058,7 @@ __metadata: react-native: 0.75.3 react-native-builder-bob: ^0.30.2 react-native-dotenv: ^3.4.11 - react-native-gesture-handler: ^2.20.1 + react-native-gesture-handler: ^2.25.0 react-native-safe-area-context: ^4.11.1 react-native-screens: ^3.34.0 react-native-vector-icons: ^10.2.0 @@ -12732,18 +12732,17 @@ __metadata: languageName: node linkType: hard -"react-native-gesture-handler@npm:^2.20.1": - version: 2.20.1 - resolution: "react-native-gesture-handler@npm:2.20.1" +"react-native-gesture-handler@npm:^2.25.0": + version: 2.25.0 + resolution: "react-native-gesture-handler@npm:2.25.0" dependencies: "@egjs/hammerjs": ^2.0.17 hoist-non-react-statics: ^3.3.0 invariant: ^2.2.4 - prop-types: ^15.7.2 peerDependencies: react: "*" react-native: "*" - checksum: 5d4414ca0c06cc9a1ed704285c2d01a5afd77400914de3374657a777c07de74fc9d0c6bf1fda2ad2f0f1cb2aebed7eaa29a0ce8707d40ab67055abf613e2adc8 + checksum: 51b155bcc56043b9e06bf8ded089c44348e8da297b38e73f203f2d6ce8bd51698f85a38dca8eeab3d2ab721aeddefc270acd99e16cf493b20f1f429ad02214b8 languageName: node linkType: hard From 7f986aa424314325c15b2a289c15b870eb18d67a Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 4 Jun 2025 13:20:33 -0700 Subject: [PATCH 04/14] feat: implement architecture switching and enhance build configurations for new architecture support --- .github/workflows/ci.yml | 172 ++++++++++++++++-- android/gradle.properties | 36 ++++ example/ios/Podfile | 21 ++- example/ios/Podfile.lock | 115 ++++++------ .../project.pbxproj | 122 ++++++++----- ios/RNIterableAPI.xcodeproj/project.pbxproj | 7 + .../RNIterable-Bridging-Header.h | 5 + package.json | 19 +- switch-arch.js | 33 ++++ 9 files changed, 397 insertions(+), 133 deletions(-) create mode 100644 switch-arch.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c7ff3c96..5671708ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: - name: Run unit tests run: yarn test --maxWorkers=2 --coverage - build-library: + build-library-old-arch: runs-on: ubuntu-latest steps: - name: Checkout @@ -47,13 +47,90 @@ jobs: - name: Setup uses: ./.github/actions/setup - - name: Build package - run: yarn prepare + - name: Switch to old architecture + run: yarn switch-arch old - build-android: + - name: Build package with old architecture + run: yarn build:old-arch + + build-library-new-arch: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + + - name: Switch to new architecture + run: yarn switch-arch new + + - name: Build package with new architecture + run: yarn build:new-arch + + build-android-old-arch: + runs-on: ubuntu-latest + env: + TURBO_CACHE_DIR: .turbo/android-old + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + + - name: Switch to old architecture + run: yarn switch-arch old + + - name: Cache turborepo for Android + uses: actions/cache@v3 + with: + path: ${{ env.TURBO_CACHE_DIR }} + key: ${{ runner.os }}-turborepo-android-old-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-turborepo-android-old- + + - name: Check turborepo cache for Android + run: | + TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status") + + if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then + echo "turbo_cache_hit=1" >> $GITHUB_ENV + fi + + - name: Install JDK + if: env.turbo_cache_hit != 1 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - name: Finalize Android SDK + if: env.turbo_cache_hit != 1 + run: | + /bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null" + + - name: Cache Gradle + if: env.turbo_cache_hit != 1 + uses: actions/cache@v3 + with: + path: | + ~/.gradle/wrapper + ~/.gradle/caches + key: ${{ runner.os }}-gradle-old-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle-old- + + - name: Build example for Android (Old Architecture) + env: + JAVA_OPTS: '-XX:MaxHeapSize=6g' + run: | + yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" + + build-android-new-arch: runs-on: ubuntu-latest env: - TURBO_CACHE_DIR: .turbo/android + TURBO_CACHE_DIR: .turbo/android-new steps: - name: Checkout uses: actions/checkout@v3 @@ -61,13 +138,16 @@ jobs: - name: Setup uses: ./.github/actions/setup + - name: Switch to new architecture + run: yarn switch-arch new + - name: Cache turborepo for Android uses: actions/cache@v3 with: path: ${{ env.TURBO_CACHE_DIR }} - key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }} + key: ${{ runner.os }}-turborepo-android-new-${{ hashFiles('yarn.lock') }} restore-keys: | - ${{ runner.os }}-turborepo-android- + ${{ runner.os }}-turborepo-android-new- - name: Check turborepo cache for Android run: | @@ -96,20 +176,20 @@ jobs: path: | ~/.gradle/wrapper ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }} + key: ${{ runner.os }}-gradle-new-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }} restore-keys: | - ${{ runner.os }}-gradle- + ${{ runner.os }}-gradle-new- - - name: Build example for Android + - name: Build example for Android (New Architecture) env: - JAVA_OPTS: "-XX:MaxHeapSize=6g" + JAVA_OPTS: '-XX:MaxHeapSize=6g' run: | yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" - build-ios: + build-ios-old-arch: runs-on: macos-14 env: - TURBO_CACHE_DIR: .turbo/ios + TURBO_CACHE_DIR: .turbo/ios-old steps: - name: Checkout uses: actions/checkout@v3 @@ -117,13 +197,69 @@ jobs: - name: Setup uses: ./.github/actions/setup + - name: Switch to old architecture + run: yarn switch-arch old + + - name: Cache turborepo for iOS + uses: actions/cache@v3 + with: + path: ${{ env.TURBO_CACHE_DIR }} + key: ${{ runner.os }}-turborepo-ios-old-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-turborepo-ios-old- + + - name: Check turborepo cache for iOS + run: | + TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status") + + if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then + echo "turbo_cache_hit=1" >> $GITHUB_ENV + fi + + - name: Cache cocoapods + if: env.turbo_cache_hit != 1 + id: cocoapods-cache + uses: actions/cache@v3 + with: + path: | + **/ios/Pods + key: ${{ runner.os }}-cocoapods-old-${{ hashFiles('example/ios/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-cocoapods-old- + + - name: Install cocoapods + if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' + run: | + cd example/ios + pod install + env: + NO_FLIPPER: 1 + + - name: Build example for iOS (Old Architecture) + run: | + yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" + + build-ios-new-arch: + runs-on: macos-14 + env: + TURBO_CACHE_DIR: .turbo/ios-new + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + + - name: Switch to new architecture + run: yarn switch-arch new + - name: Cache turborepo for iOS uses: actions/cache@v3 with: path: ${{ env.TURBO_CACHE_DIR }} - key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }} + key: ${{ runner.os }}-turborepo-ios-new-${{ hashFiles('yarn.lock') }} restore-keys: | - ${{ runner.os }}-turborepo-ios- + ${{ runner.os }}-turborepo-ios-new- - name: Check turborepo cache for iOS run: | @@ -140,9 +276,9 @@ jobs: with: path: | **/ios/Pods - key: ${{ runner.os }}-cocoapods-${{ hashFiles('example/ios/Podfile.lock') }} + key: ${{ runner.os }}-cocoapods-new-${{ hashFiles('example/ios/Podfile.lock') }} restore-keys: | - ${{ runner.os }}-cocoapods- + ${{ runner.os }}-cocoapods-new- - name: Install cocoapods if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' @@ -152,6 +288,6 @@ jobs: env: NO_FLIPPER: 1 - - name: Build example for iOS + - name: Build example for iOS (New Architecture) run: | yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" diff --git a/android/gradle.properties b/android/gradle.properties index 9a7463450..b57aface4 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -5,3 +5,39 @@ RNIterable_compileSdkVersion=31 RNIterable_ndkversion=21.4.7075529 android.useAndroidX=true android.enableJetifier=true + +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true + +# Version of flipper SDK to use with React Native +FLIPPER_VERSION=0.182.0 + +# Use this property to specify which architecture you want to build. +# Options are "old" or "new" +newArchEnabled=false + +# Use this property to enable the Hermes VM. +# If set to false, you will be using JSC instead. +hermesEnabled=true diff --git a/example/ios/Podfile b/example/ios/Podfile index e0fcf5ba0..4a119accf 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -11,11 +11,8 @@ prepare_react_native_project! # Enable new architecture ENV['RCT_NEW_ARCH_ENABLED'] = '1' -linkage = ENV['USE_FRAMEWORKS'] -if linkage != nil - Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green - use_frameworks! :linkage => linkage.to_sym -end +# Use frameworks for all pods +use_frameworks! :linkage => :static target 'ReactNativeSdkExample' do config = use_native_modules! @@ -30,6 +27,8 @@ target 'ReactNativeSdkExample' do :hermes_enabled => true ) + pod 'IterableSDK' + target 'ReactNativeSdkExampleTests' do inherit! :complete # Pods for testing @@ -40,8 +39,16 @@ target 'ReactNativeSdkExample' do react_native_post_install( installer, config[:reactNativePath], - :mac_catalyst_enabled => false, - # :ccache_enabled => true + :mac_catalyst_enabled => false ) + + # Fix for Xcode 15 and new architecture + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.2' + config.build_settings['ENABLE_BITCODE'] = 'NO' + config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES' + end + end end end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 366ccc4d8..0db7cca84 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -30,6 +30,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - IterableSDK (5.0.3) - RCT-Folly (2024.01.01.00): - boost - DoubleConversion @@ -1617,6 +1618,7 @@ PODS: - React-Core - React-debug - React-Fabric + - React-FabricComponents - React-featureflags - React-graphics - React-ImageManager @@ -1705,6 +1707,7 @@ DEPENDENCIES: - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - Iterable-React-Native-SDK (from `../..`) + - IterableSDK - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) @@ -1772,6 +1775,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - Iterable-iOS-SDK + - IterableSDK - SocketRocket EXTERNAL SOURCES: @@ -1921,70 +1925,71 @@ SPEC CHECKSUMS: glog: 69ef571f3de08433d766d614c73a9838a06bf7eb hermes-engine: 8d2103d6c0176779aea4e25df6bb1410f9946680 Iterable-iOS-SDK: 710bf6dada7dae9c03a09db7f0bc6c9d9a671f40 - Iterable-React-Native-SDK: 3f7f37ca61709b01328990dc7be823c43df52bd3 + Iterable-React-Native-SDK: 5c24de61a0453025d312532c813f83e9e595047c + IterableSDK: 37fbabbb86220d46c912d5aaff944f759c17f3a2 RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740 RCTDeprecation: 4191f6e64b72d9743f6fe1a8a16e89e868f5e9e7 RCTRequired: 9bb589570f2bb3abc6518761e3fd1ad9b7f7f06c RCTTypeSafety: 1c1a8741c86df0a0ac1a99cf3fb0e29eedbc2c88 React: b6810a201ee11e69ae8bfd4eb4aaab86610600bf React-callinvoker: d6c7898b63e6a2d37bc308f17c05be0ba3630b10 - React-Core: 411ef2293ba0c9147e04ee89324bc1575b38a089 - React-CoreModules: 30c44229d249317498dac4a984925c56e06f61c2 - React-cxxreact: 1ba92740ea3ed5be86898dec22f6548aa843da16 - React-debug: fd0ed8ecd5f8a23c7daf5ceaca8aa722a4d083fd - React-defaultsnativemodule: b013b8fc528e0830c0397250bd565e54c6eb3b90 - React-domnativemodule: 1e0a9cdd989be6905ff5867ab4b060e3be3900e4 - React-Fabric: da5caca65022dcbbb95d12cebcde7770fdd44ff0 - React-FabricComponents: 19e0eb8be8d8f2afa1a921705a87d8818eba14cf - React-FabricImage: 50df9e6aace1781cf23e130171631d7674b4072c - React-featureflags: cf78861db9318ae29982fa8953c92d31b276c9ac - React-featureflagsnativemodule: 8a6f72b439f2f48d80d0d52f4a2d30769fdd4727 - React-graphics: 7ed2dc99f706228448b870882729a8303343b5a5 - React-hermes: 167b427c2106b92ac47add9b35ca024d42453518 - React-idlecallbacksnativemodule: 26d5b4ce0389063f9659e8ebe6a14f87e7affdfb - React-ImageManager: 9970421c57b6458d3a4d6ce319c9067217c4882f - React-jserrorhandler: 6764a4b7abd617332fb0935c9ba63a6369207a15 - React-jsi: 7713fae6d70c49a1b1b12d7e65ca62a50cd820d2 - React-jsiexecutor: 67260e3eb3d1f3d3fd41ff15e89ce4027ae9c36a - React-jsinspector: a0f1febb0bcf5770ff135444a6afee7520ee42f7 - React-jsitracing: bf77e00063522e4fd6d84fa129f0caaf360d275e - React-logger: 7e56c9eceafd7f45e98c16cb42ff3c9966c67119 - React-Mapbuffer: e68dd904f0f3a84dd35989288ed3bcf5e37f9737 - React-microtasksnativemodule: 2b22a88c8abe1a90aca1804e55717190d3a26cee - react-native-safe-area-context: 17800bbaacdd18289d68ac8d4eb541c1a183463f - react-native-webview: 4cfaa2237960fb97ad5de782cf97ea475bf09af4 - React-nativeconfig: 4a9543185905fe41014c06776bf126083795aed9 - React-NativeModulesApple: f6b6dc0998c945dd113858f1fc12e5e5f0da0990 + React-Core: 2fc97900b68e7568233698c6113ca9d64ed8b520 + React-CoreModules: 2d68c251bc4080028f2835fa47504e8f20669a21 + React-cxxreact: 5f233f8ac7ea4772e49462e0ab2b0a15a4f80ab7 + React-debug: 16366fffc0cf8914d4e69a103b2b9d06b051daa7 + React-defaultsnativemodule: dde6cc8db974581a799bc066efce836ffa0d9005 + React-domnativemodule: 4d05bb76fed291db794fbe347b0b44716a11a184 + React-Fabric: 517472a46d326349902e33e63bcde2133178eb73 + React-FabricComponents: 70a60d6194db2d50b887a2c5dfb4144950242438 + React-FabricImage: 8400b691b0cf5728458a15f5d70a034e0e01c7f1 + React-featureflags: 3b80a592aec905c62bcea96f219f33a88f5a8ea3 + React-featureflagsnativemodule: 4b421eb1cb534c473567c9cbfa11e60c1745642c + React-graphics: 281e54a228ae0541b74e428394b4ac54d2210a2b + React-hermes: 95c27801c60615345ee6256eafa6d597ce983b8b + React-idlecallbacksnativemodule: e657563d73beca42333a7e5c21246fbc53cbb529 + React-ImageManager: a1ea46ce239f0fcd1df471c35dc559c8b26f31d8 + React-jserrorhandler: f5e00daad1d80feb67f3400cf8bf25f782b34b67 + React-jsi: d77bb442a4b0849063f2bd22d3c1fa71918713b7 + React-jsiexecutor: 3b9c6334b7b0f42d4c4aae950132766e63a7809f + React-jsinspector: 4579d72bf210a9028038d64099ece0261d691e01 + React-jsitracing: 11e3a39d0b47c8ba84aa94d137a7a11a6074a82b + React-logger: 4072f39df335ca443932e0ccece41fbeb5ca8404 + React-Mapbuffer: a90dc2b770956814f2f2ac4553063c5ce2618fe8 + React-microtasksnativemodule: 0b44bb6d632c420b02ec90a7375c208a0e7896ab + react-native-safe-area-context: 1194c10b089e866195f1e437cfd371e83c195589 + react-native-webview: b20d2755d69776b5e254822e8c8f452694f9d2b2 + React-nativeconfig: cd572c97f64676dcc2a1c88fc644b693b98408bf + React-NativeModulesApple: 249c7a23d8ad82f13778f4f525a8d19ccf4deb86 React-perflogger: 3bbb82f18e9ac29a1a6931568e99d6305ef4403b - React-performancetimeline: 05c0372923c2f3a9e8a5ae954258f0436003bffb + React-performancetimeline: 2274e28dc1bd9b22c70913fdf93203ea3988a126 React-RCTActionSheet: cb2b38a53d03ec22f1159c89667b86c2c490d92d - React-RCTAnimation: c8be4f58eabb487d6346247ee8e7bac434737ed7 - React-RCTAppDelegate: 357634ff6819214e37538458053213fccc940967 - React-RCTBlob: 7a64271f64a60390a2e73edecaca2735be8044ff - React-RCTFabric: dcdea7c1ff2fa7ec2875a855f8e6ce228950884d - React-RCTImage: 4fb571875362a78ccc01aded76b94a71ae466b8b - React-RCTLinking: e825182eaf7f4047f6bb11bb6cd2ae5858008e66 - React-RCTNetwork: 0e07b83395b6ff5016f7cea4ac99426a893a1438 - React-RCTSettings: bd68792732f116994e992cf48e5bb70c4eb3910e - React-RCTText: c3cfce62ddb887cdd86403a6130a58a1f8fed9f3 - React-RCTVibration: 32a10228b7affa8de6401dba6f0d73b5a8433342 - React-rendererconsistency: 993f54bb0df644df2922cd87ea55238d510d992b - React-rendererdebug: 9cd1f3e6d12c1d9b99fce6ceb373495b29b3d9ee - React-rncore: 1df26fe0ae861c599f9f2896f45e8834ef4b85f9 - React-RuntimeApple: 5fb9053ae46ec14407f24547afd903ec8f0c0b9a - React-RuntimeCore: f6af8417106c1ce5b494edd99e86b0d0069ad6c3 + React-RCTAnimation: 6836c87c7364f471e9077fda80b7349bc674be33 + React-RCTAppDelegate: 603240f6a7d7eefeeffe4e29dd0be70dc35208cf + React-RCTBlob: 516dbbd38397f5013394fdd1cc65408cc82e37a1 + React-RCTFabric: 2389d69b7c6862c5a9494bc3e1200ea56c9c81b7 + React-RCTImage: 1b2c2c1716db859ffff2d7a06a30b0ec5c677fc5 + React-RCTLinking: 59c07577767e705b0ab95d11e5ad74c61bf2a022 + React-RCTNetwork: f9a827e7d6bc428e0d99cd1fbe0427854354b8c1 + React-RCTSettings: 614252fecc24840f61590c016aca1664a52cfb0f + React-RCTText: 424549f68867265aa25969f50e7b9bf8bd70ae55 + React-RCTVibration: c8d156e6cce18f00b0310db7670fa997c7cda407 + React-rendererconsistency: e5f034a362659ade5e7de13e41b5d77c0b9f5c1e + React-rendererdebug: 4b06fadff173e3cdc1cefa45fc4dfe5ca087415b + React-rncore: 639c8dde2af07944446a829dd784e19332890a38 + React-RuntimeApple: 2c958a446af933ea6542bd39893e0eef0ecb979e + React-RuntimeCore: 84296af62ff91b7c693771e09e09899ab6afe80b React-runtimeexecutor: 9a668b94ad5d93755443311715bd57680330286a - React-RuntimeHermes: 6c6053fb5aca5558b071cfbce2868ca50a3b8fc7 - React-runtimescheduler: 5d1a32712d441c38e6d5815069e1810d38ed26f7 - React-utils: 3c815e7d3abb801930a3df2db870c92855429fea - ReactCodegen: 3d11bcf0cac47a77042a3476a1c2f7058bfd6880 - ReactCommon: c65f7049a542669dcc7bff6b7a8071a039c7d0dd - RNGestureHandler: 8dab066435239ff9f56934a2dae9080734cfec2b - RNScreens: b941e3ee5fe1991bd25098b5fc8d72db88678793 - RNVectorIcons: a24016b773380b1aa37fca501ec6b94a951890a0 + React-RuntimeHermes: 06edca92bc368d5529be75663741f76de7923b01 + React-runtimescheduler: 3d2c3ef1c7c20268a129651d0b85ca151e168a0f + React-utils: 211020f1a54caa72455ccf55853b064e590f564b + ReactCodegen: 4291c041b116b97f7203d961a3cc95adf2347064 + ReactCommon: d5e70af52fb05bd546fd7f002c549f57116c59e4 + RNGestureHandler: 11c5dd2f721813fe21291182e636e3b84eb90ded + RNScreens: 47d5b0b48402f5a2da5b2dd543deae3c9083a5ac + RNVectorIcons: 8c7af262a73890caabbac2bb081ddbaa1b8a768c SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d - Yoga: 4ef80d96a5534f0e01b3055f17d1e19a9fc61b63 + Yoga: 33604ac44957ebe3f30f15b4cd0a3f96634e624a -PODFILE CHECKSUM: 4c6a6a09af0b3466691c09d9d9af2515b974ddc9 +PODFILE CHECKSUM: 5f90f4d7c265f880f7fd67f1e8ebaca813846945 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj index 0284e8e1f..074199af7 100644 --- a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj +++ b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj @@ -8,11 +8,11 @@ /* Begin PBXBuildFile section */ 00E356F31AD99517003FC87E /* ReactNativeSdkExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* ReactNativeSdkExampleTests.m */; }; - 0C80B921A6F3F58F76C31292 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-ReactNativeSdkExample.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; - 7699B88040F8A987B510C191 /* libPods-ReactNativeSdkExample-ReactNativeSdkExampleTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-ReactNativeSdkExample-ReactNativeSdkExampleTests.a */; }; + 2A3E6B2C5E705A7EDF686E84 /* Pods_ReactNativeSdkExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05E876B5261C6746E1A77B06 /* Pods_ReactNativeSdkExample.framework */; }; + 75E0FCF4B434AB4B3BC1160B /* Pods_ReactNativeSdkExample_ReactNativeSdkExampleTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7CA06A41BBE122C9FCB3514 /* Pods_ReactNativeSdkExample_ReactNativeSdkExampleTests.framework */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; A3A40C20801B8F02005FA4C0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */; }; /* End PBXBuildFile section */ @@ -31,6 +31,7 @@ 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 = ""; }; 00E356F21AD99517003FC87E /* ReactNativeSdkExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactNativeSdkExampleTests.m; sourceTree = ""; }; + 05E876B5261C6746E1A77B06 /* Pods_ReactNativeSdkExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReactNativeSdkExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07F961A680F5B00A75B9A /* ReactNativeSdkExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReactNativeSdkExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = ReactNativeSdkExample/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = ReactNativeSdkExample/AppDelegate.mm; sourceTree = ""; }; @@ -38,14 +39,13 @@ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ReactNativeSdkExample/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = ReactNativeSdkExample/main.m; sourceTree = ""; }; 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 19F6CBCC0A4E27FBF8BF4A61 /* libPods-ReactNativeSdkExample-ReactNativeSdkExampleTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample-ReactNativeSdkExampleTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 14CAA8009BEAF5BC437B0010 /* 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 = ""; }; 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 3B4392A12AC88292D35C810B /* 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 = ""; }; - 5709B34CF0A7D63546082F79 /* 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 = ""; }; - 5B7EB9410499542E8C5724F5 /* Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.debug.xcconfig"; sourceTree = ""; }; - 5DCACB8F33CDC322A6C60F78 /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B9AD4590D711AC8FE0EDDC9 /* 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 = ""; }; + 3C67BBC613DBECB7B754E015 /* Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.debug.xcconfig"; sourceTree = ""; }; + 6E8A570F33713FDF3AAC4551 /* Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.release.xcconfig"; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReactNativeSdkExample/LaunchScreen.storyboard; sourceTree = ""; }; - 89C6BE57DB24E9ADA2F236DE /* Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.release.xcconfig"; sourceTree = ""; }; + A7CA06A41BBE122C9FCB3514 /* Pods_ReactNativeSdkExample_ReactNativeSdkExampleTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReactNativeSdkExample_ReactNativeSdkExampleTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -54,7 +54,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7699B88040F8A987B510C191 /* libPods-ReactNativeSdkExample-ReactNativeSdkExampleTests.a in Frameworks */, + 75E0FCF4B434AB4B3BC1160B /* Pods_ReactNativeSdkExample_ReactNativeSdkExampleTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -62,7 +62,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0C80B921A6F3F58F76C31292 /* libPods-ReactNativeSdkExample.a in Frameworks */, + 2A3E6B2C5E705A7EDF686E84 /* Pods_ReactNativeSdkExample.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -105,8 +105,8 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 5DCACB8F33CDC322A6C60F78 /* libPods-ReactNativeSdkExample.a */, - 19F6CBCC0A4E27FBF8BF4A61 /* libPods-ReactNativeSdkExample-ReactNativeSdkExampleTests.a */, + 05E876B5261C6746E1A77B06 /* Pods_ReactNativeSdkExample.framework */, + A7CA06A41BBE122C9FCB3514 /* Pods_ReactNativeSdkExample_ReactNativeSdkExampleTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -145,10 +145,10 @@ BBD78D7AC51CEA395F1C20DB /* Pods */ = { isa = PBXGroup; children = ( - 3B4392A12AC88292D35C810B /* Pods-ReactNativeSdkExample.debug.xcconfig */, - 5709B34CF0A7D63546082F79 /* Pods-ReactNativeSdkExample.release.xcconfig */, - 5B7EB9410499542E8C5724F5 /* Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.debug.xcconfig */, - 89C6BE57DB24E9ADA2F236DE /* Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.release.xcconfig */, + 3B9AD4590D711AC8FE0EDDC9 /* Pods-ReactNativeSdkExample.debug.xcconfig */, + 14CAA8009BEAF5BC437B0010 /* Pods-ReactNativeSdkExample.release.xcconfig */, + 3C67BBC613DBECB7B754E015 /* Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.debug.xcconfig */, + 6E8A570F33713FDF3AAC4551 /* Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -160,12 +160,12 @@ isa = PBXNativeTarget; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "ReactNativeSdkExampleTests" */; buildPhases = ( - A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */, + 6CEECA2297B65FEA3E3A0A5F /* [CP] Check Pods Manifest.lock */, 00E356EA1AD99517003FC87E /* Sources */, 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, - C59DA0FBD6956966B86A3779 /* [CP] Embed Pods Frameworks */, - F6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */, + B6F5333CD2C49B6E805773FC /* [CP] Embed Pods Frameworks */, + 1B6A03EDB91FE20BE49C8BD1 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -181,13 +181,13 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ReactNativeSdkExample" */; buildPhases = ( - C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */, + 5BA701BAE553BB4F805F9754 /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */, - E235C05ADACE081382539298 /* [CP] Copy Pods Resources */, + 45FCB159FC0EA01CA5688140 /* [CP] Embed Pods Frameworks */, + EA19910410B762283E74B168 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -271,7 +271,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"; }; - 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = { + 1B6A03EDB91FE20BE49C8BD1 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 45FCB159FC0EA01CA5688140 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -288,7 +305,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */ = { + 5BA701BAE553BB4F805F9754 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -303,14 +320,14 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests-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; }; - C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = { + 6CEECA2297B65FEA3E3A0A5F /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -325,14 +342,14 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ReactNativeSdkExample-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests-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; }; - C59DA0FBD6956966B86A3779 /* [CP] Embed Pods Frameworks */ = { + B6F5333CD2C49B6E805773FC /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -349,7 +366,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = { + EA19910410B762283E74B168 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -366,23 +383,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources.sh\"\n"; showEnvVarsInLog = 0; }; - F6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests/Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -416,7 +416,7 @@ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5B7EB9410499542E8C5724F5 /* Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.debug.xcconfig */; + baseConfigurationReference = 3C67BBC613DBECB7B754E015 /* Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -443,7 +443,7 @@ }; 00E356F71AD99517003FC87E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 89C6BE57DB24E9ADA2F236DE /* Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.release.xcconfig */; + baseConfigurationReference = 6E8A570F33713FDF3AAC4551 /* Pods-ReactNativeSdkExample-ReactNativeSdkExampleTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; COPY_PHASE_STRIP = NO; @@ -467,7 +467,7 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-ReactNativeSdkExample.debug.xcconfig */; + baseConfigurationReference = 3B9AD4590D711AC8FE0EDDC9 /* Pods-ReactNativeSdkExample.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -494,7 +494,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-ReactNativeSdkExample.release.xcconfig */; + baseConfigurationReference = 14CAA8009BEAF5BC437B0010 /* Pods-ReactNativeSdkExample.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -568,6 +568,17 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD = ""; LDPLUSPLUS = ""; @@ -645,6 +656,17 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD = ""; LDPLUSPLUS = ""; diff --git a/ios/RNIterableAPI.xcodeproj/project.pbxproj b/ios/RNIterableAPI.xcodeproj/project.pbxproj index 64c960da7..dec88348e 100644 --- a/ios/RNIterableAPI.xcodeproj/project.pbxproj +++ b/ios/RNIterableAPI.xcodeproj/project.pbxproj @@ -192,6 +192,13 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + DEFINES_MODULE = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + ENABLE_BITCODE = NO; + SWIFT_VERSION = 5.0; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_INSTALL_OBJC_HEADER = NO; }; name = Debug; }; diff --git a/ios/RNIterableAPI/RNIterable-Bridging-Header.h b/ios/RNIterableAPI/RNIterable-Bridging-Header.h index 35adb4320..63e9385e6 100644 --- a/ios/RNIterableAPI/RNIterable-Bridging-Header.h +++ b/ios/RNIterableAPI/RNIterable-Bridging-Header.h @@ -6,3 +6,8 @@ #import #import #import +#import +#import +#import +#import +#import diff --git a/package.json b/package.json index fb42dc80b..e59d73527 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,11 @@ "docs": "typedoc", "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", "prepare": "yarn add_build_info; yarn bob build", - "release": "release-it" + "release": "release-it", + "build:old-arch": "yarn add_build_info; yarn bob build --no-new-arch", + "build:new-arch": "yarn add_build_info; yarn bob build --new-arch", + "build:all": "yarn build:old-arch && yarn build:new-arch", + "switch-arch": "node switch-arch.js" }, "keywords": [ "react-native", @@ -158,11 +162,20 @@ "project": "tsconfig.build.json" } ] - ] + ], + "configs": { + "old-arch": { + "newArchEnabled": false + }, + "new-arch": { + "newArchEnabled": true + } + } }, "create-react-native-library": { "type": "module-legacy", "languages": "kotlin-swift", "version": "0.41.2" - } + }, + "newArchEnabled": true } diff --git a/switch-arch.js b/switch-arch.js new file mode 100644 index 000000000..c35572df8 --- /dev/null +++ b/switch-arch.js @@ -0,0 +1,33 @@ +const fs = require('fs'); +const path = require('path'); + +const architecture = process.argv[2]; + +if (!architecture || !['old', 'new'].includes(architecture)) { + console.error('Please specify architecture: old or new'); + process.exit(1); +} + +// Update package.json +const packageJsonPath = path.join(__dirname, 'package.json'); +const packageJson = require(packageJsonPath); +packageJson.newArchEnabled = architecture === 'new'; +fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); + +// Update Android gradle.properties +const gradlePropertiesPath = path.join( + __dirname, + 'android', + 'gradle.properties' +); +let gradleProperties = fs.readFileSync(gradlePropertiesPath, 'utf8'); +gradleProperties = gradleProperties.replace( + /newArchEnabled=.*/, + `newArchEnabled=${architecture === 'new'}` +); +fs.writeFileSync(gradlePropertiesPath, gradleProperties); + +console.log(`Switched to ${architecture} architecture configuration`); +console.log('Please run the following commands to rebuild:'); +console.log('1. yarn clean'); +console.log(`2. yarn build:${architecture}-arch`); From 196e0f6ce29f1f7a7efcbc82a03bb823c70bea1e Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 4 Jun 2025 13:23:46 -0700 Subject: [PATCH 05/14] feat: add support for Iterable URL handling and custom action handling in React Native SDK --- README.md | 64 +++++- .../IterableCustomActionHandler.java | 33 +++ .../reactnative/IterableInAppHandler.java | 59 +++++ .../reactnative/IterableUrlHandler.java | 32 +++ .../reactnative/RNIterableAPIModuleImpl.java | 214 ++++++++++++++++++ .../reactnative/RNIterableAPIModuleSpec.java | 114 ++++++++++ .../reactnative/RNIterableAPIPackage.java | 13 +- .../iterable/reactnative/Serialization.java | 182 ++++++++++++++- ios/RNIterableAPI/RNIterableAPISpec.h | 10 + ios/RNIterableAPI/RNIterableAPISpec.mm | 23 ++ 10 files changed, 731 insertions(+), 13 deletions(-) create mode 100644 android/src/main/java/com/iterable/reactnative/IterableCustomActionHandler.java create mode 100644 android/src/main/java/com/iterable/reactnative/IterableInAppHandler.java create mode 100644 android/src/main/java/com/iterable/reactnative/IterableUrlHandler.java create mode 100644 android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java create mode 100644 android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleSpec.java create mode 100644 ios/RNIterableAPI/RNIterableAPISpec.h create mode 100644 ios/RNIterableAPI/RNIterableAPISpec.mm diff --git a/README.md b/README.md index a0ccacaef..f1fe46cf8 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,69 @@ Iterable's React Native SDK relies on: ## Architecture Support -**Important**: Iterable's React Native SDK currently does not support [React Native's New Architecture](https://reactnative.dev/architecture/landing-page) (Fabric, TurboModules, and Codegen). The SDK is designed to work with the **legacy architecture only**. If you're using the New Architecture in your React Native application, you may encounter compatibility issues. +Iterable's React Native SDK supports both the legacy architecture and React Native's New Architecture (Fabric, TurboModules, and Codegen). The SDK is designed to work seamlessly with both architectures, providing flexibility for your React Native application. + +### New Architecture Support + +Starting from version 2.0.0-beta.1, the SDK supports React Native's New Architecture. To use the SDK with the New Architecture: + +1. Enable the New Architecture in your project: + ```bash + yarn switch-arch new + ``` + +2. Clean and rebuild your project: + ```bash + yarn clean + yarn build:new-arch + ``` + +3. For Android, ensure your `android/gradle.properties` has: + ```properties + newArchEnabled=true + ``` + +4. For iOS, the New Architecture will be automatically enabled when building with the new architecture flag. + +### Legacy Architecture Support + +To use the SDK with the legacy architecture: + +1. Ensure you're using the legacy architecture: + ```bash + yarn switch-arch old + ``` + +2. Clean and rebuild your project: + ```bash + yarn clean + yarn build:old-arch + ``` + +3. For Android, ensure your `android/gradle.properties` has: + ```properties + newArchEnabled=false + ``` + +### Building for Both Architectures + +If you need to support both architectures in your project: + +1. Build for both architectures: + ```bash + yarn build:all + ``` + +This will create builds for both the old and new architecture, allowing you to test and deploy your app with either architecture. + +### CI/CD Integration + +The SDK includes CI/CD configurations that test both architectures. The CI pipeline will: +- Build the library with both old and new architecture +- Build Android example app with both architectures +- Build iOS example app with both architectures + +This ensures that your SDK works correctly with both architectures in your CI/CD pipeline. ## Installation diff --git a/android/src/main/java/com/iterable/reactnative/IterableCustomActionHandler.java b/android/src/main/java/com/iterable/reactnative/IterableCustomActionHandler.java new file mode 100644 index 000000000..5ad82c4be --- /dev/null +++ b/android/src/main/java/com/iterable/reactnative/IterableCustomActionHandler.java @@ -0,0 +1,33 @@ +package com.iterable.reactnative; + +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.iterable.iterableapi.IterableAction; +import com.iterable.iterableapi.IterableActionContext; +import com.iterable.iterableapi.IterableCustomActionHandler; + +public class IterableCustomActionHandler implements IterableCustomActionHandler { + private final ReactApplicationContext reactContext; + private final ReadableMap config; + + public IterableCustomActionHandler(ReadableMap config) { + this.config = config; + this.reactContext = null; + } + + public IterableCustomActionHandler(ReadableMap config, ReactApplicationContext reactContext) { + this.config = config; + this.reactContext = reactContext; + } + + @Override + public boolean handleIterableCustomAction(IterableAction action, IterableActionContext actionContext) { + if (reactContext != null) { + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("customActionHandler", action.getType()); + } + return config.getBoolean("shouldHandleCustomAction"); + } +} diff --git a/android/src/main/java/com/iterable/reactnative/IterableInAppHandler.java b/android/src/main/java/com/iterable/reactnative/IterableInAppHandler.java new file mode 100644 index 000000000..16d5c445b --- /dev/null +++ b/android/src/main/java/com/iterable/reactnative/IterableInAppHandler.java @@ -0,0 +1,59 @@ +package com.iterable.reactnative; + +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.iterable.iterableapi.IterableInAppMessage; +import com.iterable.iterableapi.IterableInAppHandler; + +public class IterableInAppHandler implements IterableInAppHandler { + private final ReactApplicationContext reactContext; + private final ReadableMap config; + + public IterableInAppHandler(ReadableMap config) { + this.config = config; + this.reactContext = null; + } + + public IterableInAppHandler(ReadableMap config, ReactApplicationContext reactContext) { + this.config = config; + this.reactContext = reactContext; + } + + @Override + public boolean onInAppReceived(IterableInAppMessage message) { + if (reactContext != null) { + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("inAppReceived", message.getMessageId()); + } + return config.getBoolean("shouldShowInApp"); + } + + @Override + public void onInAppDisplayed(IterableInAppMessage message) { + if (reactContext != null) { + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("inAppDisplayed", message.getMessageId()); + } + } + + @Override + public void onInAppDismissed(IterableInAppMessage message) { + if (reactContext != null) { + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("inAppDismissed", message.getMessageId()); + } + } + + @Override + public void onInAppClicked(IterableInAppMessage message, String clickedUrl) { + if (reactContext != null) { + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("inAppClicked", message.getMessageId()); + } + } +} diff --git a/android/src/main/java/com/iterable/reactnative/IterableUrlHandler.java b/android/src/main/java/com/iterable/reactnative/IterableUrlHandler.java new file mode 100644 index 000000000..d43abfc97 --- /dev/null +++ b/android/src/main/java/com/iterable/reactnative/IterableUrlHandler.java @@ -0,0 +1,32 @@ +package com.iterable.reactnative; + +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.iterable.iterableapi.IterableUrlCallback; + +public class IterableUrlHandler implements IterableUrlCallback { + private final ReactApplicationContext reactContext; + private final ReadableMap config; + + public IterableUrlHandler(ReadableMap config) { + this.config = config; + this.reactContext = null; + } + + public IterableUrlHandler(ReadableMap config, ReactApplicationContext reactContext) { + this.config = config; + this.reactContext = reactContext; + } + + @Override + public boolean handleIterableURL(String url) { + if (reactContext != null) { + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("urlHandler", url); + } + return config.getBoolean("shouldOpenInNewWindow"); + } +} diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java new file mode 100644 index 000000000..eeb36b037 --- /dev/null +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -0,0 +1,214 @@ +package com.iterable.reactnative; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; +import com.iterable.iterableapi.IterableApi; +import com.iterable.iterableapi.IterableConfig; +import com.iterable.iterableapi.IterableInAppManager; +import com.iterable.iterableapi.IterableLogger; + +public class RNIterableAPIModuleImpl extends RNIterableAPIModuleSpec { + private static final String TAG = "RNIterableAPIModuleImpl"; + + public RNIterableAPIModuleImpl(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public void initializeWithApiKey(String apiKey, ReadableMap config, String version, Promise promise) { + try { + IterableConfig iterableConfig = Serialization.getIterableConfigFromReadableMap(config); + IterableApi.initialize(getReactApplicationContext(), apiKey, iterableConfig); + IterableApi.getInstance().setDeviceAttribute("reactNativeSDKVersion", version); + promise.resolve(true); + } catch (Exception e) { + promise.reject("", e.getMessage()); + } + } + + @Override + public void initialize2WithApiKey(String apiKey, ReadableMap config, String apiEndPoint, String version, Promise promise) { + try { + IterableConfig iterableConfig = Serialization.getIterableConfigFromReadableMap(config); + IterableApi.initialize(getReactApplicationContext(), apiKey, iterableConfig, apiEndPoint); + IterableApi.getInstance().setDeviceAttribute("reactNativeSDKVersion", version); + promise.resolve(true); + } catch (Exception e) { + promise.reject("", e.getMessage()); + } + } + + @Override + public void setEmail(String email, String authToken) { + IterableApi.getInstance().setEmail(email, authToken); + } + + @Override + public void getEmail(Promise promise) { + promise.resolve(IterableApi.getInstance().getEmail()); + } + + @Override + public void setUserId(String userId, String authToken) { + IterableApi.getInstance().setUserId(userId, authToken); + } + + @Override + public void getUserId(Promise promise) { + promise.resolve(IterableApi.getInstance().getUserId()); + } + + @Override + public void disableDeviceForCurrentUser() { + IterableApi.getInstance().disableDeviceForCurrentUser(); + } + + @Override + public void setInAppShowResponse(double inAppShowResponse) { + // Implementation for in-app show response + } + + @Override + public void getLastPushPayload(Promise promise) { + promise.resolve(IterableApi.getInstance().getLastPushPayload()); + } + + @Override + public void getAttributionInfo(Promise promise) { + promise.resolve(IterableApi.getInstance().getAttributionInfo()); + } + + @Override + public void setAttributionInfo(ReadableMap attributionInfo) { + IterableApi.getInstance().setAttributionInfo(Serialization.getAttributionInfoFromReadableMap(attributionInfo)); + } + + @Override + public void trackPushOpen(double campaignId, Double templateId, String messageId, boolean appAlreadyRunning, ReadableMap dataFields) { + IterableApi.getInstance().trackPushOpen( + (long)campaignId, + templateId != null ? templateId.longValue() : null, + messageId, + appAlreadyRunning, + Serialization.getMapFromReadableMap(dataFields) + ); + } + + @Override + public void updateCart(ReadableArray items) { + IterableApi.getInstance().updateCart(Serialization.getCommerceItemsFromReadableArray(items)); + } + + @Override + public void trackPurchase(double total, ReadableArray items, ReadableMap dataFields) { + IterableApi.getInstance().trackPurchase( + total, + Serialization.getCommerceItemsFromReadableArray(items), + Serialization.getMapFromReadableMap(dataFields) + ); + } + + @Override + public void trackInAppOpen(String messageId, double location) { + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + inAppManager.trackInAppOpen(messageId, Serialization.getInAppLocationFromInteger((int)location)); + } + + @Override + public void trackInAppClick(String messageId, double location, String clickedUrl) { + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + inAppManager.trackInAppClick(messageId, Serialization.getInAppLocationFromInteger((int)location), clickedUrl); + } + + @Override + public void trackInAppClose(String messageId, double location, double source, String clickedUrl) { + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + inAppManager.trackInAppClose( + messageId, + Serialization.getInAppLocationFromInteger((int)location), + Serialization.getInAppCloseSourceFromInteger((int)source), + clickedUrl + ); + } + + @Override + public void inAppConsume(String messageId, double location, double source) { + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + inAppManager.removeMessage( + messageId, + Serialization.getInAppLocationFromInteger((int)location), + Serialization.getInAppDeleteSourceFromInteger((int)source) + ); + } + + @Override + public void getHtmlInAppContentForMessage(String messageId, Promise promise) { + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + inAppManager.getHtmlInAppContentForMessage(messageId, promise::resolve, promise::reject); + } + + @Override + public void trackEvent(String name, ReadableMap dataFields) { + IterableApi.getInstance().trackEvent(name, Serialization.getMapFromReadableMap(dataFields)); + } + + @Override + public void updateUser(ReadableMap dataFields, boolean mergeNestedObjects) { + IterableApi.getInstance().updateUser(Serialization.getMapFromReadableMap(dataFields), mergeNestedObjects); + } + + @Override + public void updateEmail(String email, String authToken) { + IterableApi.getInstance().updateEmail(email, authToken); + } + + @Override + public void handleAppLink(String appLink, Promise promise) { + IterableApi.getInstance().handleAppLink(appLink, promise::resolve, promise::reject); + } + + @Override + public void getInAppMessages(Promise promise) { + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + inAppManager.getMessages(promise::resolve, promise::reject); + } + + @Override + public void getInboxMessages(Promise promise) { + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + inAppManager.getInboxMessages(promise::resolve, promise::reject); + } + + @Override + public void getUnreadInboxMessagesCount(Promise promise) { + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + inAppManager.getUnreadInboxMessagesCount(promise::resolve, promise::reject); + } + + @Override + public void showMessage(String messageId, boolean consume, Promise promise) { + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + inAppManager.showMessage(messageId, consume, promise::resolve, promise::reject); + } + + @Override + public void setReadForMessage(String messageId, boolean read) { + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + inAppManager.setRead(messageId, read); + } + + @Override + public void removeMessage(String messageId, double location, double deleteSource) { + IterableInAppManager inAppManager = IterableApi.getInstance().getInAppManager(); + inAppManager.removeMessage( + messageId, + Serialization.getInAppLocationFromInteger((int)location), + Serialization.getInAppDeleteSourceFromInteger((int)deleteSource) + ); + } +} diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleSpec.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleSpec.java new file mode 100644 index 000000000..b139de83d --- /dev/null +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleSpec.java @@ -0,0 +1,114 @@ +package com.iterable.reactnative; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; + +@ReactModule(name = RNIterableAPIModuleSpec.NAME) +public abstract class RNIterableAPIModuleSpec extends ReactContextBaseJavaModule implements TurboModule { + public static final String NAME = "RNIterableAPI"; + + public RNIterableAPIModuleSpec(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return NAME; + } + + // Native SDK Functions + @ReactMethod + public abstract void initializeWithApiKey(String apiKey, ReadableMap config, String version, Promise promise); + + @ReactMethod + public abstract void initialize2WithApiKey(String apiKey, ReadableMap config, String apiEndPoint, String version, Promise promise); + + @ReactMethod + public abstract void setEmail(String email, String authToken); + + @ReactMethod + public abstract void getEmail(Promise promise); + + @ReactMethod + public abstract void setUserId(String userId, String authToken); + + @ReactMethod + public abstract void getUserId(Promise promise); + + // Iterable API Request Functions + @ReactMethod + public abstract void disableDeviceForCurrentUser(); + + @ReactMethod + public abstract void setInAppShowResponse(double inAppShowResponse); + + @ReactMethod + public abstract void getLastPushPayload(Promise promise); + + @ReactMethod + public abstract void getAttributionInfo(Promise promise); + + @ReactMethod + public abstract void setAttributionInfo(ReadableMap attributionInfo); + + @ReactMethod + public abstract void trackPushOpen(double campaignId, Double templateId, String messageId, boolean appAlreadyRunning, ReadableMap dataFields); + + @ReactMethod + public abstract void updateCart(ReadableArray items); + + @ReactMethod + public abstract void trackPurchase(double total, ReadableArray items, ReadableMap dataFields); + + @ReactMethod + public abstract void trackInAppOpen(String messageId, double location); + + @ReactMethod + public abstract void trackInAppClick(String messageId, double location, String clickedUrl); + + @ReactMethod + public abstract void trackInAppClose(String messageId, double location, double source, String clickedUrl); + + @ReactMethod + public abstract void inAppConsume(String messageId, double location, double source); + + @ReactMethod + public abstract void getHtmlInAppContentForMessage(String messageId, Promise promise); + + @ReactMethod + public abstract void trackEvent(String name, ReadableMap dataFields); + + @ReactMethod + public abstract void updateUser(ReadableMap dataFields, boolean mergeNestedObjects); + + @ReactMethod + public abstract void updateEmail(String email, String authToken); + + @ReactMethod + public abstract void handleAppLink(String appLink, Promise promise); + + // SDK In-App Manager Functions + @ReactMethod + public abstract void getInAppMessages(Promise promise); + + @ReactMethod + public abstract void getInboxMessages(Promise promise); + + @ReactMethod + public abstract void getUnreadInboxMessagesCount(Promise promise); + + @ReactMethod + public abstract void showMessage(String messageId, boolean consume, Promise promise); + + @ReactMethod + public abstract void setReadForMessage(String messageId, boolean read); + + @ReactMethod + public abstract void removeMessage(String messageId, double location, double deleteSource); +} diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java index 2b04e447c..57c65df04 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java @@ -1,7 +1,6 @@ package com.iterable.reactnative; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -13,12 +12,9 @@ public class RNIterableAPIPackage implements ReactPackage { @Override - public List createNativeModules( - ReactApplicationContext reactContext) { + public List createNativeModules(ReactApplicationContext reactContext) { List modules = new ArrayList<>(); - - modules.add(new RNIterableAPIModule(reactContext)); - + modules.add(new RNIterableAPIModuleImpl(reactContext)); return modules; } @@ -26,4 +22,9 @@ public List createNativeModules( public List createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); } + + @Override + public List> createJSModules() { + return Collections.emptyList(); + } } diff --git a/android/src/main/java/com/iterable/reactnative/Serialization.java b/android/src/main/java/com/iterable/reactnative/Serialization.java index 3a1f536a6..b38848b24 100644 --- a/android/src/main/java/com/iterable/reactnative/Serialization.java +++ b/android/src/main/java/com/iterable/reactnative/Serialization.java @@ -24,6 +24,10 @@ import com.iterable.iterableapi.IterableInboxSession; import com.iterable.iterableapi.IterableLogger; import com.iterable.iterableapi.RNIterableInternal; +import com.iterable.iterableapi.IterableAttributionInfo; +import com.iterable.iterableapi.IterableCommerceItem; +import com.iterable.iterableapi.IterableInAppCloseSource; +import com.iterable.iterableapi.IterableInAppDeleteSource; import org.json.JSONArray; import org.json.JSONException; @@ -32,8 +36,9 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Map; -class Serialization { +public class Serialization { static String TAG = "Serialization"; static IterableInAppLocation getIterableInAppLocationFromInteger(@Nullable Integer location) { @@ -94,7 +99,7 @@ static CommerceItem commerceItemFromMap(JSONObject itemMap) throws JSONException categories[i] = categoriesArray.getString(i); } } - + return new CommerceItem(itemMap.getString("id"), itemMap.getString("name"), itemMap.getDouble("price"), @@ -216,7 +221,7 @@ static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableConte configBuilder.setDataRegion(iterableDataRegion); } - + if (iterableContextJSON.has("encryptionEnforced")) { configBuilder.setEncryptionEnforced(iterableContextJSON.optBoolean("encryptionEnforced")); } @@ -281,12 +286,10 @@ static List impressionsFromReadableArray(Readab return list; } - - // --------------------------------------------------------------------------------------- // region React Native JSON conversion methods // obtained from https://gist.github.com/viperwarp/2beb6bbefcc268dee7ad - + static WritableMap convertJsonToMap(JSONObject jsonObject) throws JSONException { WritableMap map = new WritableNativeMap(); @@ -393,4 +396,171 @@ static JSONArray convertArrayToJson(ReadableArray readableArray) throws JSONExce } // --------------------------------------------------------------------------------------- // endregion + + public static Map getMapFromReadableMap(ReadableMap readableMap) { + if (readableMap == null) { + return null; + } + + Map map = new HashMap<>(); + for (String key : readableMap.toHashMap().keySet()) { + ReadableType type = readableMap.getType(key); + switch (type) { + case Null: + map.put(key, null); + break; + case Boolean: + map.put(key, readableMap.getBoolean(key)); + break; + case Number: + map.put(key, readableMap.getDouble(key)); + break; + case String: + map.put(key, readableMap.getString(key)); + break; + case Map: + map.put(key, getMapFromReadableMap(readableMap.getMap(key))); + break; + case Array: + map.put(key, getListFromReadableArray(readableMap.getArray(key))); + break; + } + } + return map; + } + + public static List getListFromReadableArray(ReadableArray readableArray) { + if (readableArray == null) { + return null; + } + + List list = new ArrayList<>(); + for (int i = 0; i < readableArray.size(); i++) { + ReadableType type = readableArray.getType(i); + switch (type) { + case Null: + list.add(null); + break; + case Boolean: + list.add(readableArray.getBoolean(i)); + break; + case Number: + list.add(readableArray.getDouble(i)); + break; + case String: + list.add(readableArray.getString(i)); + break; + case Map: + list.add(getMapFromReadableMap(readableArray.getMap(i))); + break; + case Array: + list.add(getListFromReadableArray(readableArray.getArray(i))); + break; + } + } + return list; + } + + public static IterableConfig getIterableConfigFromReadableMap(ReadableMap config) { + if (config == null) { + return new IterableConfig.Builder().build(); + } + + IterableConfig.Builder builder = new IterableConfig.Builder(); + + if (config.hasKey("pushIntegrationName")) { + builder.setPushIntegrationName(config.getString("pushIntegrationName")); + } + + if (config.hasKey("autoPushRegistration")) { + builder.setAutoPushRegistration(config.getBoolean("autoPushRegistration")); + } + + if (config.hasKey("logLevel")) { + builder.setLogLevel(config.getInt("logLevel")); + } + + if (config.hasKey("inAppDisplayInterval")) { + builder.setInAppDisplayInterval(config.getDouble("inAppDisplayInterval")); + } + + if (config.hasKey("urlHandler")) { + builder.setUrlHandler(new IterableUrlHandler(config.getMap("urlHandler"))); + } + + if (config.hasKey("customActionHandler")) { + builder.setCustomActionHandler(new IterableCustomActionHandler(config.getMap("customActionHandler"))); + } + + if (config.hasKey("inAppHandler")) { + builder.setInAppHandler(new IterableInAppHandler(config.getMap("inAppHandler"))); + } + + return builder.build(); + } + + public static IterableAttributionInfo getAttributionInfoFromReadableMap(ReadableMap attributionInfo) { + if (attributionInfo == null) { + return null; + } + + return new IterableAttributionInfo( + attributionInfo.getString("campaignId"), + attributionInfo.getString("templateId"), + attributionInfo.getString("messageId") + ); + } + + public static List getCommerceItemsFromReadableArray(ReadableArray items) { + if (items == null) { + return null; + } + + List commerceItems = new ArrayList<>(); + for (int i = 0; i < items.size(); i++) { + ReadableMap item = items.getMap(i); + commerceItems.add(new IterableCommerceItem( + item.getString("id"), + item.getString("name"), + item.getDouble("price"), + item.getInt("quantity") + )); + } + return commerceItems; + } + + public static IterableInAppLocation getInAppLocationFromInteger(int location) { + switch (location) { + case 0: + return IterableInAppLocation.IN_APP; + case 1: + return IterableInAppLocation.INBOX; + default: + return IterableInAppLocation.IN_APP; + } + } + + public static IterableInAppCloseSource getInAppCloseSourceFromInteger(int source) { + switch (source) { + case 0: + return IterableInAppCloseSource.BACK; + case 1: + return IterableInAppCloseSource.LINK; + case 2: + return IterableInAppCloseSource.CLOSE; + default: + return IterableInAppCloseSource.BACK; + } + } + + public static IterableInAppDeleteSource getInAppDeleteSourceFromInteger(int source) { + switch (source) { + case 0: + return IterableInAppDeleteSource.INBOX_SWIPE; + case 1: + return IterableInAppDeleteSource.DELETE_BUTTON; + default: + return IterableInAppDeleteSource.INBOX_SWIPE; + } + } } diff --git a/ios/RNIterableAPI/RNIterableAPISpec.h b/ios/RNIterableAPI/RNIterableAPISpec.h new file mode 100644 index 000000000..94ff5bf3f --- /dev/null +++ b/ios/RNIterableAPI/RNIterableAPISpec.h @@ -0,0 +1,10 @@ +#import +#import +#import + +#import +#import + +@interface RNIterableAPISpec : NSObject + +@end diff --git a/ios/RNIterableAPI/RNIterableAPISpec.mm b/ios/RNIterableAPI/RNIterableAPISpec.mm new file mode 100644 index 000000000..103e50c81 --- /dev/null +++ b/ios/RNIterableAPI/RNIterableAPISpec.mm @@ -0,0 +1,23 @@ +#import "RNIterableAPISpec.h" +#import +#import +#import + +#import +#import + +@implementation RNIterableAPISpec + +RCT_EXPORT_MODULE(RNIterableAPI) + +- (std::shared_ptr)componentDescriptorForName:(NSString *)name +{ + return nullptr; +} + +- (UIView *)createViewWithName:(NSString *)name +{ + return nil; +} + +@end From 2a0c7b19919ed240e6cfb16173273dc7a04ce91e Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 4 Jun 2025 13:24:59 -0700 Subject: [PATCH 06/14] feat: change RNIterableAPISpec to extend RCTEventEmitter for improved event handling --- ios/RNIterableAPI/RNIterableAPISpec.h | 2 +- ios/RNIterableAPI/RNIterableAPISpec.mm | 372 ++++++++++++++++++++++++- 2 files changed, 362 insertions(+), 12 deletions(-) diff --git a/ios/RNIterableAPI/RNIterableAPISpec.h b/ios/RNIterableAPI/RNIterableAPISpec.h index 94ff5bf3f..884aefe11 100644 --- a/ios/RNIterableAPI/RNIterableAPISpec.h +++ b/ios/RNIterableAPI/RNIterableAPISpec.h @@ -5,6 +5,6 @@ #import #import -@interface RNIterableAPISpec : NSObject +@interface RNIterableAPISpec : RCTEventEmitter @end diff --git a/ios/RNIterableAPI/RNIterableAPISpec.mm b/ios/RNIterableAPI/RNIterableAPISpec.mm index 103e50c81..4b3d4fd86 100644 --- a/ios/RNIterableAPI/RNIterableAPISpec.mm +++ b/ios/RNIterableAPI/RNIterableAPISpec.mm @@ -1,23 +1,373 @@ #import "RNIterableAPISpec.h" -#import -#import -#import - -#import #import +#import +#import @implementation RNIterableAPISpec RCT_EXPORT_MODULE(RNIterableAPI) -- (std::shared_ptr)componentDescriptorForName:(NSString *)name -{ - return nullptr; +- (NSArray *)supportedEvents { + return @[ + @"urlHandler", + @"customActionHandler", + @"inAppReceived", + @"inAppDisplayed", + @"inAppDismissed", + @"inAppClicked" + ]; +} + +RCT_EXPORT_METHOD(initializeWithApiKey:(NSString *)apiKey + config:(NSDictionary *)config + version:(NSString *)version + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + IterableConfig *iterableConfig = [self getIterableConfigFromDictionary:config]; + [IterableAPI initializeWithApiKey:apiKey config:iterableConfig]; + [IterableAPI.sharedInstance setDeviceAttribute:@"reactNativeSDKVersion" value:version]; + resolve(@YES); +} + +RCT_EXPORT_METHOD(initialize2WithApiKey:(NSString *)apiKey + config:(NSDictionary *)config + apiEndPoint:(NSString *)apiEndPoint + version:(NSString *)version + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + IterableConfig *iterableConfig = [self getIterableConfigFromDictionary:config]; + [IterableAPI initializeWithApiKey:apiKey config:iterableConfig apiEndPoint:apiEndPoint]; + [IterableAPI.sharedInstance setDeviceAttribute:@"reactNativeSDKVersion" value:version]; + resolve(@YES); +} + +RCT_EXPORT_METHOD(setEmail:(NSString *)email authToken:(NSString *)authToken) { + [IterableAPI.sharedInstance setEmail:email authToken:authToken]; +} + +RCT_EXPORT_METHOD(getEmail:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + resolve(IterableAPI.sharedInstance.email); +} + +RCT_EXPORT_METHOD(setUserId:(NSString *)userId authToken:(NSString *)authToken) { + [IterableAPI.sharedInstance setUserId:userId authToken:authToken]; +} + +RCT_EXPORT_METHOD(getUserId:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + resolve(IterableAPI.sharedInstance.userId); +} + +RCT_EXPORT_METHOD(disableDeviceForCurrentUser) { + [IterableAPI.sharedInstance disableDeviceForCurrentUser]; +} + +RCT_EXPORT_METHOD(setInAppShowResponse:(double)inAppShowResponse) { + // Implementation for in-app show response +} + +RCT_EXPORT_METHOD(getLastPushPayload:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + resolve(IterableAPI.sharedInstance.lastPushPayload); +} + +RCT_EXPORT_METHOD(getAttributionInfo:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + resolve([self dictionaryFromAttributionInfo:IterableAPI.sharedInstance.attributionInfo]); +} + +RCT_EXPORT_METHOD(setAttributionInfo:(NSDictionary *)attributionInfo) { + [IterableAPI.sharedInstance setAttributionInfo:[self attributionInfoFromDictionary:attributionInfo]]; +} + +RCT_EXPORT_METHOD(trackPushOpen:(double)campaignId + templateId:(NSNumber *)templateId + messageId:(NSString *)messageId + appAlreadyRunning:(BOOL)appAlreadyRunning + dataFields:(NSDictionary *)dataFields) { + [IterableAPI.sharedInstance trackPushOpen:messageId + campaignId:(NSInteger)campaignId + templateId:templateId + appAlreadyRunning:appAlreadyRunning + dataFields:dataFields]; +} + +RCT_EXPORT_METHOD(updateCart:(NSArray *)items) { + [IterableAPI.sharedInstance updateCart:[self commerceItemsFromArray:items]]; +} + +RCT_EXPORT_METHOD(trackPurchase:(double)total + items:(NSArray *)items + dataFields:(NSDictionary *)dataFields) { + [IterableAPI.sharedInstance trackPurchase:total + items:[self commerceItemsFromArray:items] + dataFields:dataFields]; +} + +RCT_EXPORT_METHOD(trackInAppOpen:(NSString *)messageId location:(double)location) { + [IterableAPI.sharedInstance.inAppManager trackInAppOpen:messageId + location:[self inAppLocationFromInteger:(int)location]]; +} + +RCT_EXPORT_METHOD(trackInAppClick:(NSString *)messageId + location:(double)location + clickedUrl:(NSString *)clickedUrl) { + [IterableAPI.sharedInstance.inAppManager trackInAppClick:messageId + location:[self inAppLocationFromInteger:(int)location] + clickedUrl:clickedUrl]; +} + +RCT_EXPORT_METHOD(trackInAppClose:(NSString *)messageId + location:(double)location + source:(double)source + clickedUrl:(NSString *)clickedUrl) { + [IterableAPI.sharedInstance.inAppManager trackInAppClose:messageId + location:[self inAppLocationFromInteger:(int)location] + source:[self inAppCloseSourceFromInteger:(int)source] + clickedUrl:clickedUrl]; +} + +RCT_EXPORT_METHOD(inAppConsume:(NSString *)messageId + location:(double)location + source:(double)source) { + [IterableAPI.sharedInstance.inAppManager removeMessage:messageId + location:[self inAppLocationFromInteger:(int)location] + source:[self inAppDeleteSourceFromInteger:(int)source]]; +} + +RCT_EXPORT_METHOD(getHtmlInAppContentForMessage:(NSString *)messageId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + [IterableAPI.sharedInstance.inAppManager getHtmlInAppContentForMessage:messageId + onSuccess:^(NSString *html) { + resolve(html); + } onFailure:^(NSString *reason) { + reject(@"", reason, nil); + }]; +} + +RCT_EXPORT_METHOD(trackEvent:(NSString *)name dataFields:(NSDictionary *)dataFields) { + [IterableAPI.sharedInstance trackEvent:name dataFields:dataFields]; +} + +RCT_EXPORT_METHOD(updateUser:(NSDictionary *)dataFields mergeNestedObjects:(BOOL)mergeNestedObjects) { + [IterableAPI.sharedInstance updateUser:dataFields mergeNestedObjects:mergeNestedObjects]; +} + +RCT_EXPORT_METHOD(updateEmail:(NSString *)email authToken:(NSString *)authToken) { + [IterableAPI.sharedInstance updateEmail:email authToken:authToken]; +} + +RCT_EXPORT_METHOD(handleAppLink:(NSString *)appLink + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + [IterableAPI.sharedInstance handleAppLink:appLink + onSuccess:^(NSDictionary *data) { + resolve(data); + } onFailure:^(NSString *reason) { + reject(@"", reason, nil); + }]; +} + +RCT_EXPORT_METHOD(getInAppMessages:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + [IterableAPI.sharedInstance.inAppManager getMessages:^(NSArray *messages) { + resolve([self arrayFromInAppMessages:messages]); + } onFailure:^(NSString *reason) { + reject(@"", reason, nil); + }]; +} + +RCT_EXPORT_METHOD(getInboxMessages:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + [IterableAPI.sharedInstance.inAppManager getInboxMessages:^(NSArray *messages) { + resolve([self arrayFromInAppMessages:messages]); + } onFailure:^(NSString *reason) { + reject(@"", reason, nil); + }]; +} + +RCT_EXPORT_METHOD(getUnreadInboxMessagesCount:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + [IterableAPI.sharedInstance.inAppManager getUnreadInboxMessagesCount:^(NSInteger count) { + resolve(@(count)); + } onFailure:^(NSString *reason) { + reject(@"", reason, nil); + }]; +} + +RCT_EXPORT_METHOD(showMessage:(NSString *)messageId + consume:(BOOL)consume + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + [IterableAPI.sharedInstance.inAppManager showMessage:messageId + consume:consume + onSuccess:^(NSDictionary *data) { + resolve(data); + } onFailure:^(NSString *reason) { + reject(@"", reason, nil); + }]; +} + +RCT_EXPORT_METHOD(setReadForMessage:(NSString *)messageId read:(BOOL)read) { + [IterableAPI.sharedInstance.inAppManager setRead:messageId read:read]; +} + +RCT_EXPORT_METHOD(removeMessage:(NSString *)messageId + location:(double)location + deleteSource:(double)deleteSource) { + [IterableAPI.sharedInstance.inAppManager removeMessage:messageId + location:[self inAppLocationFromInteger:(int)location] + source:[self inAppDeleteSourceFromInteger:(int)deleteSource]]; +} + +#pragma mark - Helper Methods + +- (IterableConfig *)getIterableConfigFromDictionary:(NSDictionary *)config { + IterableConfig *iterableConfig = [[IterableConfig alloc] init]; + + if (config[@"pushIntegrationName"]) { + iterableConfig.pushIntegrationName = config[@"pushIntegrationName"]; + } + + if (config[@"autoPushRegistration"]) { + iterableConfig.autoPushRegistration = [config[@"autoPushRegistration"] boolValue]; + } + + if (config[@"logLevel"]) { + iterableConfig.logLevel = [config[@"logLevel"] integerValue]; + } + + if (config[@"inAppDisplayInterval"]) { + iterableConfig.inAppDisplayInterval = [config[@"inAppDisplayInterval"] doubleValue]; + } + + if (config[@"urlHandler"]) { + iterableConfig.urlHandler = ^(NSURL *url) { + [self sendEventWithName:@"urlHandler" body:url.absoluteString]; + return [config[@"urlHandler"][@"shouldOpenInNewWindow"] boolValue]; + }; + } + + if (config[@"customActionHandler"]) { + iterableConfig.customActionHandler = ^(IterableAction *action, IterableActionContext *context) { + [self sendEventWithName:@"customActionHandler" body:action.type]; + return [config[@"customActionHandler"][@"shouldHandleCustomAction"] boolValue]; + }; + } + + if (config[@"inAppHandler"]) { + iterableConfig.inAppHandler = [[IterableInAppHandler alloc] init]; + iterableConfig.inAppHandler.onInAppReceived = ^(IterableInAppMessage *message) { + [self sendEventWithName:@"inAppReceived" body:message.messageId]; + return [config[@"inAppHandler"][@"shouldShowInApp"] boolValue]; + }; + iterableConfig.inAppHandler.onInAppDisplayed = ^(IterableInAppMessage *message) { + [self sendEventWithName:@"inAppDisplayed" body:message.messageId]; + }; + iterableConfig.inAppHandler.onInAppDismissed = ^(IterableInAppMessage *message) { + [self sendEventWithName:@"inAppDismissed" body:message.messageId]; + }; + iterableConfig.inAppHandler.onInAppClicked = ^(IterableInAppMessage *message, NSString *clickedUrl) { + [self sendEventWithName:@"inAppClicked" body:message.messageId]; + }; + } + + return iterableConfig; +} + +- (NSDictionary *)dictionaryFromAttributionInfo:(IterableAttributionInfo *)attributionInfo { + if (!attributionInfo) { + return nil; + } + + return @{ + @"campaignId": attributionInfo.campaignId ?: [NSNull null], + @"templateId": attributionInfo.templateId ?: [NSNull null], + @"messageId": attributionInfo.messageId ?: [NSNull null] + }; +} + +- (IterableAttributionInfo *)attributionInfoFromDictionary:(NSDictionary *)dictionary { + if (!dictionary) { + return nil; + } + + return [[IterableAttributionInfo alloc] initWithCampaignId:dictionary[@"campaignId"] + templateId:dictionary[@"templateId"] + messageId:dictionary[@"messageId"]]; +} + +- (NSArray *)commerceItemsFromArray:(NSArray *)items { + NSMutableArray *commerceItems = [NSMutableArray array]; + + for (NSDictionary *item in items) { + IterableCommerceItem *commerceItem = [[IterableCommerceItem alloc] initWithId:item[@"id"] + name:item[@"name"] + price:[item[@"price"] doubleValue] + quantity:[item[@"quantity"] integerValue]]; + [commerceItems addObject:commerceItem]; + } + + return commerceItems; +} + +- (NSArray *)arrayFromInAppMessages:(NSArray *)messages { + NSMutableArray *array = [NSMutableArray array]; + + for (IterableInAppMessage *message in messages) { + [array addObject:@{ + @"messageId": message.messageId, + @"campaignId": @(message.campaignId), + @"content": @{ + @"html": message.content.html ?: [NSNull null], + @"edgeInsets": @{ + @"top": @(message.content.padding.top), + @"left": @(message.content.padding.left), + @"bottom": @(message.content.padding.bottom), + @"right": @(message.content.padding.right) + } + } + }]; + } + + return array; +} + +- (IterableInAppLocation)inAppLocationFromInteger:(int)location { + switch (location) { + case 0: + return IterableInAppLocationInApp; + case 1: + return IterableInAppLocationInbox; + default: + return IterableInAppLocationInApp; + } +} + +- (IterableInAppCloseSource)inAppCloseSourceFromInteger:(int)source { + switch (source) { + case 0: + return IterableInAppCloseSourceBack; + case 1: + return IterableInAppCloseSourceLink; + case 2: + return IterableInAppCloseSourceClose; + default: + return IterableInAppCloseSourceBack; + } } -- (UIView *)createViewWithName:(NSString *)name -{ - return nil; +- (IterableInAppDeleteSource)inAppDeleteSourceFromInteger:(int)source { + switch (source) { + case 0: + return IterableInAppDeleteSourceInboxSwipe; + case 1: + return IterableInAppDeleteSourceDeleteButton; + default: + return IterableInAppDeleteSourceInboxSwipe; + } } @end From 5ab696cc00a8be388f00c06431aaa3207e604768 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 4 Jun 2025 13:26:13 -0700 Subject: [PATCH 07/14] feat: implement RCTTurboModule support in RNIterableAPISpec for enhanced performance --- ios/RNIterableAPI/RNIterableAPISpec.h | 3 ++- ios/RNIterableAPI/RNIterableAPISpec.mm | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ios/RNIterableAPI/RNIterableAPISpec.h b/ios/RNIterableAPI/RNIterableAPISpec.h index 884aefe11..e2626c6aa 100644 --- a/ios/RNIterableAPI/RNIterableAPISpec.h +++ b/ios/RNIterableAPI/RNIterableAPISpec.h @@ -4,7 +4,8 @@ #import #import +#import -@interface RNIterableAPISpec : RCTEventEmitter +@interface RNIterableAPISpec : RCTEventEmitter @end diff --git a/ios/RNIterableAPI/RNIterableAPISpec.mm b/ios/RNIterableAPI/RNIterableAPISpec.mm index 4b3d4fd86..95c044314 100644 --- a/ios/RNIterableAPI/RNIterableAPISpec.mm +++ b/ios/RNIterableAPI/RNIterableAPISpec.mm @@ -1,12 +1,19 @@ #import "RNIterableAPISpec.h" #import #import +#import #import @implementation RNIterableAPISpec RCT_EXPORT_MODULE(RNIterableAPI) +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + - (NSArray *)supportedEvents { return @[ @"urlHandler", From 9a61dbb782b66d661a905aec0c474935a6e1415c Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 4 Jun 2025 13:27:18 -0700 Subject: [PATCH 08/14] feat: add RNIterableInboxView component with customizable properties for inbox display --- ios/RNIterableAPI/RNIterableInboxView.h | 11 +++ ios/RNIterableAPI/RNIterableInboxView.mm | 69 +++++++++++++++++++ .../RNIterableInboxViewComponentDescriptor.h | 31 +++++++++ .../RNIterableInboxViewProps.cpp | 19 +++++ ios/RNIterableAPI/RNIterableInboxViewProps.h | 23 +++++++ 5 files changed, 153 insertions(+) create mode 100644 ios/RNIterableAPI/RNIterableInboxView.h create mode 100644 ios/RNIterableAPI/RNIterableInboxView.mm create mode 100644 ios/RNIterableAPI/RNIterableInboxViewComponentDescriptor.h create mode 100644 ios/RNIterableAPI/RNIterableInboxViewProps.cpp create mode 100644 ios/RNIterableAPI/RNIterableInboxViewProps.h diff --git a/ios/RNIterableAPI/RNIterableInboxView.h b/ios/RNIterableAPI/RNIterableInboxView.h new file mode 100644 index 000000000..8ec53cf69 --- /dev/null +++ b/ios/RNIterableAPI/RNIterableInboxView.h @@ -0,0 +1,11 @@ +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RNIterableInboxView : RCTViewComponentView + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/RNIterableAPI/RNIterableInboxView.mm b/ios/RNIterableAPI/RNIterableInboxView.mm new file mode 100644 index 000000000..c05659b77 --- /dev/null +++ b/ios/RNIterableAPI/RNIterableInboxView.mm @@ -0,0 +1,69 @@ +#import "RNIterableInboxView.h" +#import +#import +#import +#import + +using namespace facebook::react; + +@implementation RNIterableInboxView { + IterableInboxViewController *_inboxViewController; +} + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + + _inboxViewController = [[IterableInboxViewController alloc] init]; + [self addSubview:_inboxViewController.view]; + } + return self; +} + +- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps { + const auto &oldViewProps = *std::static_pointer_cast(_props); + const auto &newViewProps = *std::static_pointer_cast(props); + + [super updateProps:props oldProps:oldProps]; + + // Update inbox view controller properties + if (oldViewProps.noMessagesTitle != newViewProps.noMessagesTitle) { + _inboxViewController.noMessagesTitle = RCTNSStringFromString(newViewProps.noMessagesTitle); + } + + if (oldViewProps.noMessagesBody != newViewProps.noMessagesBody) { + _inboxViewController.noMessagesBody = RCTNSStringFromString(newViewProps.noMessagesBody); + } + + if (oldViewProps.showDate != newViewProps.showDate) { + _inboxViewController.showDate = newViewProps.showDate; + } + + if (oldViewProps.showUnreadBadge != newViewProps.showUnreadBadge) { + _inboxViewController.showUnreadBadge = newViewProps.showUnreadBadge; + } + + if (oldViewProps.showInboxTitle != newViewProps.showInboxTitle) { + _inboxViewController.showInboxTitle = newViewProps.showInboxTitle; + } + + if (oldViewProps.inboxTitle != newViewProps.inboxTitle) { + _inboxViewController.inboxTitle = RCTNSStringFromString(newViewProps.inboxTitle); + } + + if (oldViewProps.showInboxOnAppear != newViewProps.showInboxOnAppear) { + _inboxViewController.showInboxOnAppear = newViewProps.showInboxOnAppear; + } +} + +- (void)layoutSubviews { + [super layoutSubviews]; + _inboxViewController.view.frame = self.bounds; +} + +@end + +Class RNIterableInboxViewCls(void) { + return RNIterableInboxView.class; +} diff --git a/ios/RNIterableAPI/RNIterableInboxViewComponentDescriptor.h b/ios/RNIterableAPI/RNIterableInboxViewComponentDescriptor.h new file mode 100644 index 000000000..170d3479b --- /dev/null +++ b/ios/RNIterableAPI/RNIterableInboxViewComponentDescriptor.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include "RNIterableInboxViewProps.h" + +namespace facebook::react { + +class RNIterableInboxViewComponentDescriptor final + : public ViewComponentDescriptor { + public: + RNIterableInboxViewComponentDescriptor( + ComponentDescriptorParameters const ¶meters) + : ViewComponentDescriptor(parameters) {} + + void adopt(ShadowNode::Unshared const &shadowNode) const override { + concreteShadowNodeFromShadowNode(shadowNode); + ViewComponentDescriptor::adopt(shadowNode); + } + + protected: + void appendChild( + ShadowNode::Shared const &parentShadowNode, + ShadowNode::Shared const &childShadowNode) const override { + ViewComponentDescriptor::appendChild(parentShadowNode, childShadowNode); + } + + private: + friend class RNIterableInboxViewShadowNode; +}; + +} // namespace facebook::react diff --git a/ios/RNIterableAPI/RNIterableInboxViewProps.cpp b/ios/RNIterableAPI/RNIterableInboxViewProps.cpp new file mode 100644 index 000000000..32accf6c4 --- /dev/null +++ b/ios/RNIterableAPI/RNIterableInboxViewProps.cpp @@ -0,0 +1,19 @@ +#include "RNIterableInboxViewProps.h" +#include + +namespace facebook::react { + +RNIterableInboxViewProps::RNIterableInboxViewProps( + const PropsParserContext& context, + const RNIterableInboxViewProps &sourceProps, + const RawProps &rawProps) + : ViewProps(context, sourceProps, rawProps), + noMessagesTitle(convertRawProp(context, rawProps, "noMessagesTitle", sourceProps.noMessagesTitle, {})), + noMessagesBody(convertRawProp(context, rawProps, "noMessagesBody", sourceProps.noMessagesBody, {})), + showDate(convertRawProp(context, rawProps, "showDate", sourceProps.showDate, true)), + showUnreadBadge(convertRawProp(context, rawProps, "showUnreadBadge", sourceProps.showUnreadBadge, true)), + showInboxTitle(convertRawProp(context, rawProps, "showInboxTitle", sourceProps.showInboxTitle, true)), + inboxTitle(convertRawProp(context, rawProps, "inboxTitle", sourceProps.inboxTitle, {})), + showInboxOnAppear(convertRawProp(context, rawProps, "showInboxOnAppear", sourceProps.showInboxOnAppear, true)) {} + +} // namespace facebook::react diff --git a/ios/RNIterableAPI/RNIterableInboxViewProps.h b/ios/RNIterableAPI/RNIterableInboxViewProps.h new file mode 100644 index 000000000..7b4a29d1a --- /dev/null +++ b/ios/RNIterableAPI/RNIterableInboxViewProps.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +struct RNIterableInboxViewProps : public ViewProps { + RNIterableInboxViewProps() = default; + RNIterableInboxViewProps(const PropsParserContext& context, const RNIterableInboxViewProps &sourceProps, const RawProps &rawProps); + + std::string noMessagesTitle{}; + std::string noMessagesBody{}; + bool showDate{true}; + bool showUnreadBadge{true}; + bool showInboxTitle{true}; + std::string inboxTitle{}; + bool showInboxOnAppear{true}; +}; + +} // namespace facebook::react From 363f490862a1974f6adf2e66b5cd1da45b792cca Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 4 Jun 2025 13:30:57 -0700 Subject: [PATCH 09/14] feat: add RNIterableInboxView and RNIterableInboxViewCompat --- ios/RNIterableAPI/RNIterableAPICompat.h | 86 ++++ ios/RNIterableAPI/RNIterableAPICompat.m | 374 ++++++++++++++++++ ios/RNIterableAPI/RNIterableInboxViewCompat.h | 23 ++ ios/RNIterableAPI/RNIterableInboxViewCompat.m | 93 +++++ .../RNIterableInboxViewManager.h | 5 + .../RNIterableInboxViewManager.m | 26 ++ 6 files changed, 607 insertions(+) create mode 100644 ios/RNIterableAPI/RNIterableAPICompat.h create mode 100644 ios/RNIterableAPI/RNIterableAPICompat.m create mode 100644 ios/RNIterableAPI/RNIterableInboxViewCompat.h create mode 100644 ios/RNIterableAPI/RNIterableInboxViewCompat.m create mode 100644 ios/RNIterableAPI/RNIterableInboxViewManager.h create mode 100644 ios/RNIterableAPI/RNIterableInboxViewManager.m diff --git a/ios/RNIterableAPI/RNIterableAPICompat.h b/ios/RNIterableAPI/RNIterableAPICompat.h new file mode 100644 index 000000000..4870b13d3 --- /dev/null +++ b/ios/RNIterableAPI/RNIterableAPICompat.h @@ -0,0 +1,86 @@ +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RNIterableAPICompat : RCTEventEmitter + +// Initialization +- (void)initializeWithApiKey:(NSString *)apiKey + config:(NSDictionary *)config + version:(NSString *)version + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject; + +- (void)initialize2WithApiKey:(NSString *)apiKey + config:(NSDictionary *)config + apiEndPoint:(NSString *)apiEndPoint + version:(NSString *)version + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject; + +// User Management +- (void)setEmail:(NSString *)email authToken:(NSString *)authToken; +- (void)getEmail:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject; +- (void)setUserId:(NSString *)userId authToken:(NSString *)authToken; +- (void)getUserId:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject; +- (void)disableDeviceForCurrentUser; + +// Push Notifications +- (void)getLastPushPayload:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject; +- (void)trackPushOpen:(double)campaignId + templateId:(NSNumber *)templateId + messageId:(NSString *)messageId + appAlreadyRunning:(BOOL)appAlreadyRunning + dataFields:(NSDictionary *)dataFields; + +// In-App Messages +- (void)getInAppMessages:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject; +- (void)getInboxMessages:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject; +- (void)getUnreadInboxMessagesCount:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject; +- (void)showMessage:(NSString *)messageId + consume:(BOOL)consume + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject; +- (void)setReadForMessage:(NSString *)messageId read:(BOOL)read; +- (void)removeMessage:(NSString *)messageId + location:(double)location + deleteSource:(double)deleteSource; + +// Events and User Attributes +- (void)trackEvent:(NSString *)name dataFields:(NSDictionary *)dataFields; +- (void)updateUser:(NSDictionary *)dataFields mergeNestedObjects:(BOOL)mergeNestedObjects; +- (void)updateEmail:(NSString *)email authToken:(NSString *)authToken; + +// Attribution +- (void)getAttributionInfo:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject; +- (void)setAttributionInfo:(NSDictionary *)attributionInfo; + +// Commerce +- (void)updateCart:(NSArray *)items; +- (void)trackPurchase:(double)total + items:(NSArray *)items + dataFields:(NSDictionary *)dataFields; + +// In-App Tracking +- (void)trackInAppOpen:(NSString *)messageId location:(double)location; +- (void)trackInAppClick:(NSString *)messageId + location:(double)location + clickedUrl:(NSString *)clickedUrl; +- (void)trackInAppClose:(NSString *)messageId + location:(double)location + source:(double)source + clickedUrl:(NSString *)clickedUrl; +- (void)inAppConsume:(NSString *)messageId + location:(double)location + source:(double)source; + +// App Links +- (void)handleAppLink:(NSString *)appLink + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/RNIterableAPI/RNIterableAPICompat.m b/ios/RNIterableAPI/RNIterableAPICompat.m new file mode 100644 index 000000000..6fa890109 --- /dev/null +++ b/ios/RNIterableAPI/RNIterableAPICompat.m @@ -0,0 +1,374 @@ +#import "RNIterableAPICompat.h" +#import + +@implementation RNIterableAPICompat + +RCT_EXPORT_MODULE(RNIterableAPI) + +- (NSArray *)supportedEvents { + return @[ + @"urlHandler", + @"customActionHandler", + @"inAppReceived", + @"inAppDisplayed", + @"inAppDismissed", + @"inAppClicked" + ]; +} + +#pragma mark - Initialization + +RCT_EXPORT_METHOD(initializeWithApiKey:(NSString *)apiKey + config:(NSDictionary *)config + version:(NSString *)version + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + IterableConfig *iterableConfig = [self getIterableConfigFromDictionary:config]; + [IterableAPI initializeWithApiKey:apiKey config:iterableConfig]; + [IterableAPI.sharedInstance setDeviceAttribute:@"reactNativeSDKVersion" value:version]; + resolve(@YES); +} + +RCT_EXPORT_METHOD(initialize2WithApiKey:(NSString *)apiKey + config:(NSDictionary *)config + apiEndPoint:(NSString *)apiEndPoint + version:(NSString *)version + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + IterableConfig *iterableConfig = [self getIterableConfigFromDictionary:config]; + [IterableAPI initializeWithApiKey:apiKey config:iterableConfig apiEndPoint:apiEndPoint]; + [IterableAPI.sharedInstance setDeviceAttribute:@"reactNativeSDKVersion" value:version]; + resolve(@YES); +} + +#pragma mark - User Management + +RCT_EXPORT_METHOD(setEmail:(NSString *)email authToken:(NSString *)authToken) { + [IterableAPI.sharedInstance setEmail:email authToken:authToken]; +} + +RCT_EXPORT_METHOD(getEmail:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + resolve(IterableAPI.sharedInstance.email); +} + +RCT_EXPORT_METHOD(setUserId:(NSString *)userId authToken:(NSString *)authToken) { + [IterableAPI.sharedInstance setUserId:userId authToken:authToken]; +} + +RCT_EXPORT_METHOD(getUserId:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + resolve(IterableAPI.sharedInstance.userId); +} + +RCT_EXPORT_METHOD(disableDeviceForCurrentUser) { + [IterableAPI.sharedInstance disableDeviceForCurrentUser]; +} + +#pragma mark - Push Notifications + +RCT_EXPORT_METHOD(getLastPushPayload:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + resolve(IterableAPI.sharedInstance.lastPushPayload); +} + +RCT_EXPORT_METHOD(trackPushOpen:(double)campaignId + templateId:(NSNumber *)templateId + messageId:(NSString *)messageId + appAlreadyRunning:(BOOL)appAlreadyRunning + dataFields:(NSDictionary *)dataFields) { + [IterableAPI.sharedInstance trackPushOpen:messageId + campaignId:(NSInteger)campaignId + templateId:templateId + appAlreadyRunning:appAlreadyRunning + dataFields:dataFields]; +} + +#pragma mark - In-App Messages + +RCT_EXPORT_METHOD(getInAppMessages:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + [IterableAPI.sharedInstance.inAppManager getMessages:^(NSArray *messages) { + resolve([self arrayFromInAppMessages:messages]); + } onFailure:^(NSString *reason) { + reject(@"", reason, nil); + }]; +} + +RCT_EXPORT_METHOD(getInboxMessages:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + [IterableAPI.sharedInstance.inAppManager getInboxMessages:^(NSArray *messages) { + resolve([self arrayFromInAppMessages:messages]); + } onFailure:^(NSString *reason) { + reject(@"", reason, nil); + }]; +} + +RCT_EXPORT_METHOD(getUnreadInboxMessagesCount:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + [IterableAPI.sharedInstance.inAppManager getUnreadInboxMessagesCount:^(NSInteger count) { + resolve(@(count)); + } onFailure:^(NSString *reason) { + reject(@"", reason, nil); + }]; +} + +RCT_EXPORT_METHOD(showMessage:(NSString *)messageId + consume:(BOOL)consume + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + [IterableAPI.sharedInstance.inAppManager showMessage:messageId + consume:consume + onSuccess:^(NSDictionary *data) { + resolve(data); + } onFailure:^(NSString *reason) { + reject(@"", reason, nil); + }]; +} + +RCT_EXPORT_METHOD(setReadForMessage:(NSString *)messageId read:(BOOL)read) { + [IterableAPI.sharedInstance.inAppManager setRead:messageId read:read]; +} + +RCT_EXPORT_METHOD(removeMessage:(NSString *)messageId + location:(double)location + deleteSource:(double)deleteSource) { + [IterableAPI.sharedInstance.inAppManager removeMessage:messageId + location:[self inAppLocationFromInteger:(int)location] + source:[self inAppDeleteSourceFromInteger:(int)deleteSource]]; +} + +#pragma mark - Events and User Attributes + +RCT_EXPORT_METHOD(trackEvent:(NSString *)name dataFields:(NSDictionary *)dataFields) { + [IterableAPI.sharedInstance trackEvent:name dataFields:dataFields]; +} + +RCT_EXPORT_METHOD(updateUser:(NSDictionary *)dataFields mergeNestedObjects:(BOOL)mergeNestedObjects) { + [IterableAPI.sharedInstance updateUser:dataFields mergeNestedObjects:mergeNestedObjects]; +} + +RCT_EXPORT_METHOD(updateEmail:(NSString *)email authToken:(NSString *)authToken) { + [IterableAPI.sharedInstance updateEmail:email authToken:authToken]; +} + +#pragma mark - Attribution + +RCT_EXPORT_METHOD(getAttributionInfo:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + resolve([self dictionaryFromAttributionInfo:IterableAPI.sharedInstance.attributionInfo]); +} + +RCT_EXPORT_METHOD(setAttributionInfo:(NSDictionary *)attributionInfo) { + [IterableAPI.sharedInstance setAttributionInfo:[self attributionInfoFromDictionary:attributionInfo]]; +} + +#pragma mark - Commerce + +RCT_EXPORT_METHOD(updateCart:(NSArray *)items) { + [IterableAPI.sharedInstance updateCart:[self commerceItemsFromArray:items]]; +} + +RCT_EXPORT_METHOD(trackPurchase:(double)total + items:(NSArray *)items + dataFields:(NSDictionary *)dataFields) { + [IterableAPI.sharedInstance trackPurchase:total + items:[self commerceItemsFromArray:items] + dataFields:dataFields]; +} + +#pragma mark - In-App Tracking + +RCT_EXPORT_METHOD(trackInAppOpen:(NSString *)messageId location:(double)location) { + [IterableAPI.sharedInstance.inAppManager trackInAppOpen:messageId + location:[self inAppLocationFromInteger:(int)location]]; +} + +RCT_EXPORT_METHOD(trackInAppClick:(NSString *)messageId + location:(double)location + clickedUrl:(NSString *)clickedUrl) { + [IterableAPI.sharedInstance.inAppManager trackInAppClick:messageId + location:[self inAppLocationFromInteger:(int)location] + clickedUrl:clickedUrl]; +} + +RCT_EXPORT_METHOD(trackInAppClose:(NSString *)messageId + location:(double)location + source:(double)source + clickedUrl:(NSString *)clickedUrl) { + [IterableAPI.sharedInstance.inAppManager trackInAppClose:messageId + location:[self inAppLocationFromInteger:(int)location] + source:[self inAppCloseSourceFromInteger:(int)source] + clickedUrl:clickedUrl]; +} + +RCT_EXPORT_METHOD(inAppConsume:(NSString *)messageId + location:(double)location + source:(double)source) { + [IterableAPI.sharedInstance.inAppManager removeMessage:messageId + location:[self inAppLocationFromInteger:(int)location] + source:[self inAppDeleteSourceFromInteger:(int)source]]; +} + +#pragma mark - App Links + +RCT_EXPORT_METHOD(handleAppLink:(NSString *)appLink + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + [IterableAPI.sharedInstance handleAppLink:appLink + onSuccess:^(NSDictionary *data) { + resolve(data); + } onFailure:^(NSString *reason) { + reject(@"", reason, nil); + }]; +} + +#pragma mark - Helper Methods + +- (IterableConfig *)getIterableConfigFromDictionary:(NSDictionary *)config { + IterableConfig *iterableConfig = [[IterableConfig alloc] init]; + + if (config[@"pushIntegrationName"]) { + iterableConfig.pushIntegrationName = config[@"pushIntegrationName"]; + } + + if (config[@"autoPushRegistration"]) { + iterableConfig.autoPushRegistration = [config[@"autoPushRegistration"] boolValue]; + } + + if (config[@"logLevel"]) { + iterableConfig.logLevel = [config[@"logLevel"] integerValue]; + } + + if (config[@"inAppDisplayInterval"]) { + iterableConfig.inAppDisplayInterval = [config[@"inAppDisplayInterval"] doubleValue]; + } + + if (config[@"urlHandler"]) { + iterableConfig.urlHandler = ^(NSURL *url) { + [self sendEventWithName:@"urlHandler" body:url.absoluteString]; + return [config[@"urlHandler"][@"shouldOpenInNewWindow"] boolValue]; + }; + } + + if (config[@"customActionHandler"]) { + iterableConfig.customActionHandler = ^(IterableAction *action, IterableActionContext *context) { + [self sendEventWithName:@"customActionHandler" body:action.type]; + return [config[@"customActionHandler"][@"shouldHandleCustomAction"] boolValue]; + }; + } + + if (config[@"inAppHandler"]) { + iterableConfig.inAppHandler = [[IterableInAppHandler alloc] init]; + iterableConfig.inAppHandler.onInAppReceived = ^(IterableInAppMessage *message) { + [self sendEventWithName:@"inAppReceived" body:message.messageId]; + return [config[@"inAppHandler"][@"shouldShowInApp"] boolValue]; + }; + iterableConfig.inAppHandler.onInAppDisplayed = ^(IterableInAppMessage *message) { + [self sendEventWithName:@"inAppDisplayed" body:message.messageId]; + }; + iterableConfig.inAppHandler.onInAppDismissed = ^(IterableInAppMessage *message) { + [self sendEventWithName:@"inAppDismissed" body:message.messageId]; + }; + iterableConfig.inAppHandler.onInAppClicked = ^(IterableInAppMessage *message, NSString *clickedUrl) { + [self sendEventWithName:@"inAppClicked" body:message.messageId]; + }; + } + + return iterableConfig; +} + +- (NSDictionary *)dictionaryFromAttributionInfo:(IterableAttributionInfo *)attributionInfo { + if (!attributionInfo) { + return nil; + } + + return @{ + @"campaignId": attributionInfo.campaignId ?: [NSNull null], + @"templateId": attributionInfo.templateId ?: [NSNull null], + @"messageId": attributionInfo.messageId ?: [NSNull null] + }; +} + +- (IterableAttributionInfo *)attributionInfoFromDictionary:(NSDictionary *)dictionary { + if (!dictionary) { + return nil; + } + + return [[IterableAttributionInfo alloc] initWithCampaignId:dictionary[@"campaignId"] + templateId:dictionary[@"templateId"] + messageId:dictionary[@"messageId"]]; +} + +- (NSArray *)commerceItemsFromArray:(NSArray *)items { + NSMutableArray *commerceItems = [NSMutableArray array]; + + for (NSDictionary *item in items) { + IterableCommerceItem *commerceItem = [[IterableCommerceItem alloc] initWithId:item[@"id"] + name:item[@"name"] + price:[item[@"price"] doubleValue] + quantity:[item[@"quantity"] integerValue]]; + [commerceItems addObject:commerceItem]; + } + + return commerceItems; +} + +- (NSArray *)arrayFromInAppMessages:(NSArray *)messages { + NSMutableArray *array = [NSMutableArray array]; + + for (IterableInAppMessage *message in messages) { + [array addObject:@{ + @"messageId": message.messageId, + @"campaignId": @(message.campaignId), + @"content": @{ + @"html": message.content.html ?: [NSNull null], + @"edgeInsets": @{ + @"top": @(message.content.padding.top), + @"left": @(message.content.padding.left), + @"bottom": @(message.content.padding.bottom), + @"right": @(message.content.padding.right) + } + } + }]; + } + + return array; +} + +- (IterableInAppLocation)inAppLocationFromInteger:(int)location { + switch (location) { + case 0: + return IterableInAppLocationInApp; + case 1: + return IterableInAppLocationInbox; + default: + return IterableInAppLocationInApp; + } +} + +- (IterableInAppCloseSource)inAppCloseSourceFromInteger:(int)source { + switch (source) { + case 0: + return IterableInAppCloseSourceBack; + case 1: + return IterableInAppCloseSourceLink; + case 2: + return IterableInAppCloseSourceClose; + default: + return IterableInAppCloseSourceBack; + } +} + +- (IterableInAppDeleteSource)inAppDeleteSourceFromInteger:(int)source { + switch (source) { + case 0: + return IterableInAppDeleteSourceInboxSwipe; + case 1: + return IterableInAppDeleteSourceDeleteButton; + default: + return IterableInAppDeleteSourceInboxSwipe; + } +} + +@end diff --git a/ios/RNIterableAPI/RNIterableInboxViewCompat.h b/ios/RNIterableAPI/RNIterableInboxViewCompat.h new file mode 100644 index 000000000..3b139ed34 --- /dev/null +++ b/ios/RNIterableAPI/RNIterableInboxViewCompat.h @@ -0,0 +1,23 @@ +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RNIterableInboxViewCompat : RCTView + +@property (nonatomic, copy) NSString *noMessagesTitle; +@property (nonatomic, copy) NSString *noMessagesBody; +@property (nonatomic, assign) BOOL showDate; +@property (nonatomic, assign) BOOL showUnreadBadge; +@property (nonatomic, assign) BOOL showInboxTitle; +@property (nonatomic, copy) NSString *inboxTitle; +@property (nonatomic, assign) BOOL showInboxOnAppear; + +@property (nonatomic, copy) RCTDirectEventBlock onMessageSelected; +@property (nonatomic, copy) RCTDirectEventBlock onMessageDismissed; +@property (nonatomic, copy) RCTDirectEventBlock onInboxEmpty; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/RNIterableAPI/RNIterableInboxViewCompat.m b/ios/RNIterableAPI/RNIterableInboxViewCompat.m new file mode 100644 index 000000000..ed8c024af --- /dev/null +++ b/ios/RNIterableAPI/RNIterableInboxViewCompat.m @@ -0,0 +1,93 @@ +#import "RNIterableInboxViewCompat.h" +#import + +@implementation RNIterableInboxViewCompat { + IterableInboxViewController *_inboxViewController; +} + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + _inboxViewController = [[IterableInboxViewController alloc] init]; + _inboxViewController.delegate = self; + [self addSubview:_inboxViewController.view]; + + // Set default values + _showDate = YES; + _showUnreadBadge = YES; + _showInboxTitle = YES; + _showInboxOnAppear = YES; + } + return self; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + _inboxViewController.view.frame = self.bounds; +} + +#pragma mark - Property Setters + +- (void)setNoMessagesTitle:(NSString *)noMessagesTitle { + _noMessagesTitle = [noMessagesTitle copy]; + _inboxViewController.noMessagesTitle = noMessagesTitle; +} + +- (void)setNoMessagesBody:(NSString *)noMessagesBody { + _noMessagesBody = [noMessagesBody copy]; + _inboxViewController.noMessagesBody = noMessagesBody; +} + +- (void)setShowDate:(BOOL)showDate { + _showDate = showDate; + _inboxViewController.showDate = showDate; +} + +- (void)setShowUnreadBadge:(BOOL)showUnreadBadge { + _showUnreadBadge = showUnreadBadge; + _inboxViewController.showUnreadBadge = showUnreadBadge; +} + +- (void)setShowInboxTitle:(BOOL)showInboxTitle { + _showInboxTitle = showInboxTitle; + _inboxViewController.showInboxTitle = showInboxTitle; +} + +- (void)setInboxTitle:(NSString *)inboxTitle { + _inboxTitle = [inboxTitle copy]; + _inboxViewController.inboxTitle = inboxTitle; +} + +- (void)setShowInboxOnAppear:(BOOL)showInboxOnAppear { + _showInboxOnAppear = showInboxOnAppear; + _inboxViewController.showInboxOnAppear = showInboxOnAppear; +} + +#pragma mark - IterableInboxViewControllerDelegate + +- (void)iterableInboxViewController:(IterableInboxViewController *)viewController + didSelectMessage:(IterableInAppMessage *)message { + if (_onMessageSelected) { + _onMessageSelected(@{ + @"messageId": message.messageId, + @"campaignId": @(message.campaignId) + }); + } +} + +- (void)iterableInboxViewController:(IterableInboxViewController *)viewController + didDismissMessage:(IterableInAppMessage *)message { + if (_onMessageDismissed) { + _onMessageDismissed(@{ + @"messageId": message.messageId, + @"campaignId": @(message.campaignId) + }); + } +} + +- (void)iterableInboxViewControllerDidShowEmptyState:(IterableInboxViewController *)viewController { + if (_onInboxEmpty) { + _onInboxEmpty(nil); + } +} + +@end diff --git a/ios/RNIterableAPI/RNIterableInboxViewManager.h b/ios/RNIterableAPI/RNIterableInboxViewManager.h new file mode 100644 index 000000000..5440e70a8 --- /dev/null +++ b/ios/RNIterableAPI/RNIterableInboxViewManager.h @@ -0,0 +1,5 @@ +#import + +@interface RNIterableInboxViewManager : RCTViewManager + +@end diff --git a/ios/RNIterableAPI/RNIterableInboxViewManager.m b/ios/RNIterableAPI/RNIterableInboxViewManager.m new file mode 100644 index 000000000..a21ac7e7a --- /dev/null +++ b/ios/RNIterableAPI/RNIterableInboxViewManager.m @@ -0,0 +1,26 @@ +#import "RNIterableInboxViewManager.h" +#import "RNIterableInboxViewCompat.h" +#import +#import + +@implementation RNIterableInboxViewManager + +RCT_EXPORT_MODULE(RNIterableInboxView) + +- (UIView *)view { + return [[RNIterableInboxViewCompat alloc] init]; +} + +RCT_EXPORT_VIEW_PROPERTY(noMessagesTitle, NSString) +RCT_EXPORT_VIEW_PROPERTY(noMessagesBody, NSString) +RCT_EXPORT_VIEW_PROPERTY(showDate, BOOL) +RCT_EXPORT_VIEW_PROPERTY(showUnreadBadge, BOOL) +RCT_EXPORT_VIEW_PROPERTY(showInboxTitle, BOOL) +RCT_EXPORT_VIEW_PROPERTY(inboxTitle, NSString) +RCT_EXPORT_VIEW_PROPERTY(showInboxOnAppear, BOOL) + +RCT_EXPORT_VIEW_PROPERTY(onMessageSelected, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onMessageDismissed, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onInboxEmpty, RCTDirectEventBlock) + +@end From a89352558750a976e6c19a93f69eaa5b46218569 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 4 Jun 2025 13:32:09 -0700 Subject: [PATCH 10/14] test: add unit tests for RNIterableInboxView functionality --- ios/RNIterableAPI/RNIterableAPICompatTests.m | 304 +++++++++++++++++++ ios/RNIterableAPI/RNIterableInboxViewTests.m | 120 ++++++++ 2 files changed, 424 insertions(+) create mode 100644 ios/RNIterableAPI/RNIterableAPICompatTests.m create mode 100644 ios/RNIterableAPI/RNIterableInboxViewTests.m diff --git a/ios/RNIterableAPI/RNIterableAPICompatTests.m b/ios/RNIterableAPI/RNIterableAPICompatTests.m new file mode 100644 index 000000000..3d5238af5 --- /dev/null +++ b/ios/RNIterableAPI/RNIterableAPICompatTests.m @@ -0,0 +1,304 @@ +#import +#import "RNIterableAPICompat.h" +#import +#import + +@interface RNIterableAPICompatTests : XCTestCase + +@property (nonatomic, strong) RNIterableAPICompat *apiCompat; +@property (nonatomic, strong) id mockIterableAPI; +@property (nonatomic, strong) id mockInAppManager; + +@end + +@implementation RNIterableAPICompatTests + +- (void)setUp { + [super setUp]; + self.apiCompat = [[RNIterableAPICompat alloc] init]; + self.mockIterableAPI = OCMClassMock([IterableAPI class]); + self.mockInAppManager = OCMClassMock([IterableInAppManager class]); + OCMStub([self.mockIterableAPI sharedInstance]).andReturn(self.mockIterableAPI); + OCMStub([self.mockIterableAPI inAppManager]).andReturn(self.mockInAppManager); +} + +- (void)tearDown { + self.apiCompat = nil; + self.mockIterableAPI = nil; + self.mockInAppManager = nil; + [super tearDown]; +} + +#pragma mark - Initialization Tests + +- (void)testInitializeWithApiKey { + // Given + NSString *apiKey = @"test-api-key"; + NSDictionary *config = @{ + @"pushIntegrationName": @"test-push", + @"autoPushRegistration": @YES, + @"logLevel": @1, + @"inAppDisplayInterval": @30 + }; + NSString *version = @"1.0.0"; + + // When + XCTestExpectation *expectation = [self expectationWithDescription:@"Initialize"]; + [self.apiCompat initializeWithApiKey:apiKey + config:config + version:version + resolver:^(id result) { + // Then + XCTAssertTrue([result boolValue]); + OCMVerify([self.mockIterableAPI initializeWithApiKey:apiKey config:[OCMArg any]]); + OCMVerify([self.mockIterableAPI setDeviceAttribute:@"reactNativeSDKVersion" value:version]); + [expectation fulfill]; + } rejecter:^(NSString *code, NSString *message, NSError *error) { + XCTFail(@"Should not reject"); + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +#pragma mark - User Management Tests + +- (void)testSetEmail { + // Given + NSString *email = @"test@example.com"; + NSString *authToken = @"test-token"; + + // When + [self.apiCompat setEmail:email authToken:authToken]; + + // Then + OCMVerify([self.mockIterableAPI setEmail:email authToken:authToken]); +} + +- (void)testGetEmail { + // Given + NSString *expectedEmail = @"test@example.com"; + OCMStub([self.mockIterableAPI email]).andReturn(expectedEmail); + + // When + XCTestExpectation *expectation = [self expectationWithDescription:@"Get Email"]; + [self.apiCompat getEmail:^(id result) { + // Then + XCTAssertEqualObjects(result, expectedEmail); + [expectation fulfill]; + } rejecter:^(NSString *code, NSString *message, NSError *error) { + XCTFail(@"Should not reject"); + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +#pragma mark - Push Notification Tests + +- (void)testTrackPushOpen { + // Given + double campaignId = 123; + NSNumber *templateId = @456; + NSString *messageId = @"test-message"; + BOOL appAlreadyRunning = YES; + NSDictionary *dataFields = @{@"key": @"value"}; + + // When + [self.apiCompat trackPushOpen:campaignId + templateId:templateId + messageId:messageId + appAlreadyRunning:appAlreadyRunning + dataFields:dataFields]; + + // Then + OCMVerify([self.mockIterableAPI trackPushOpen:messageId + campaignId:(NSInteger)campaignId + templateId:templateId + appAlreadyRunning:appAlreadyRunning + dataFields:dataFields]); +} + +#pragma mark - In-App Message Tests + +- (void)testGetInAppMessages { + // Given + NSArray *expectedMessages = @[@"message1", @"message2"]; + OCMStub([self.mockInAppManager getMessages:[OCMArg any] onFailure:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + void (^successBlock)(NSArray *); + [invocation getArgument:&successBlock atIndex:2]; + successBlock(expectedMessages); + }); + + // When + XCTestExpectation *expectation = [self expectationWithDescription:@"Get In-App Messages"]; + [self.apiCompat getInAppMessages:^(id result) { + // Then + XCTAssertEqualObjects(result, expectedMessages); + [expectation fulfill]; + } rejecter:^(NSString *code, NSString *message, NSError *error) { + XCTFail(@"Should not reject"); + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +- (void)testShowMessage { + // Given + NSString *messageId = @"test-message"; + BOOL consume = YES; + NSDictionary *expectedData = @{@"key": @"value"}; + + OCMStub([self.mockInAppManager showMessage:messageId + consume:consume + onSuccess:[OCMArg any] + onFailure:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + void (^successBlock)(NSDictionary *); + [invocation getArgument:&successBlock atIndex:4]; + successBlock(expectedData); + }); + + // When + XCTestExpectation *expectation = [self expectationWithDescription:@"Show Message"]; + [self.apiCompat showMessage:messageId + consume:consume + resolver:^(id result) { + // Then + XCTAssertEqualObjects(result, expectedData); + [expectation fulfill]; + } rejecter:^(NSString *code, NSString *message, NSError *error) { + XCTFail(@"Should not reject"); + }]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +#pragma mark - Event Tracking Tests + +- (void)testTrackEvent { + // Given + NSString *eventName = @"test-event"; + NSDictionary *dataFields = @{@"key": @"value"}; + + // When + [self.apiCompat trackEvent:eventName dataFields:dataFields]; + + // Then + OCMVerify([self.mockIterableAPI trackEvent:eventName dataFields:dataFields]); +} + +#pragma mark - Commerce Tests + +- (void)testTrackPurchase { + // Given + double total = 99.99; + NSArray *items = @[ + @{ + @"id": @"item1", + @"name": @"Test Item 1", + @"price": @49.99, + @"quantity": @1 + }, + @{ + @"id": @"item2", + @"name": @"Test Item 2", + @"price": @50.00, + @"quantity": @1 + } + ]; + NSDictionary *dataFields = @{@"key": @"value"}; + + // When + [self.apiCompat trackPurchase:total items:items dataFields:dataFields]; + + // Then + OCMVerify([self.mockIterableAPI trackPurchase:total + items:[OCMArg any] + dataFields:dataFields]); +} + +#pragma mark - Helper Method Tests + +- (void)testGetIterableConfigFromDictionary { + // Given + NSDictionary *config = @{ + @"pushIntegrationName": @"test-push", + @"autoPushRegistration": @YES, + @"logLevel": @1, + @"inAppDisplayInterval": @30, + @"urlHandler": @{ + @"shouldOpenInNewWindow": @YES + }, + @"customActionHandler": @{ + @"shouldHandleCustomAction": @YES + }, + @"inAppHandler": @{ + @"shouldShowInApp": @YES + } + }; + + // When + IterableConfig *iterableConfig = [self.apiCompat getIterableConfigFromDictionary:config]; + + // Then + XCTAssertEqualObjects(iterableConfig.pushIntegrationName, @"test-push"); + XCTAssertTrue(iterableConfig.autoPushRegistration); + XCTAssertEqual(iterableConfig.logLevel, 1); + XCTAssertEqual(iterableConfig.inAppDisplayInterval, 30); + XCTAssertNotNil(iterableConfig.urlHandler); + XCTAssertNotNil(iterableConfig.customActionHandler); + XCTAssertNotNil(iterableConfig.inAppHandler); +} + +- (void)testDictionaryFromAttributionInfo { + // Given + IterableAttributionInfo *attributionInfo = [[IterableAttributionInfo alloc] initWithCampaignId:@"campaign1" + templateId:@"template1" + messageId:@"message1"]; + + // When + NSDictionary *dictionary = [self.apiCompat dictionaryFromAttributionInfo:attributionInfo]; + + // Then + XCTAssertEqualObjects(dictionary[@"campaignId"], @"campaign1"); + XCTAssertEqualObjects(dictionary[@"templateId"], @"template1"); + XCTAssertEqualObjects(dictionary[@"messageId"], @"message1"); +} + +- (void)testCommerceItemsFromArray { + // Given + NSArray *items = @[ + @{ + @"id": @"item1", + @"name": @"Test Item 1", + @"price": @49.99, + @"quantity": @1 + }, + @{ + @"id": @"item2", + @"name": @"Test Item 2", + @"price": @50.00, + @"quantity": @2 + } + ]; + + // When + NSArray *commerceItems = [self.apiCompat commerceItemsFromArray:items]; + + // Then + XCTAssertEqual(commerceItems.count, 2); + + IterableCommerceItem *item1 = commerceItems[0]; + XCTAssertEqualObjects(item1.id, @"item1"); + XCTAssertEqualObjects(item1.name, @"Test Item 1"); + XCTAssertEqual(item1.price, 49.99); + XCTAssertEqual(item1.quantity, 1); + + IterableCommerceItem *item2 = commerceItems[1]; + XCTAssertEqualObjects(item2.id, @"item2"); + XCTAssertEqualObjects(item2.name, @"Test Item 2"); + XCTAssertEqual(item2.price, 50.00); + XCTAssertEqual(item2.quantity, 2); +} + +@end diff --git a/ios/RNIterableAPI/RNIterableInboxViewTests.m b/ios/RNIterableAPI/RNIterableInboxViewTests.m new file mode 100644 index 000000000..aa60a42c0 --- /dev/null +++ b/ios/RNIterableAPI/RNIterableInboxViewTests.m @@ -0,0 +1,120 @@ +#import +#import "RNIterableInboxView.h" +#import "RNIterableInboxViewProps.h" +#import +#import + +@interface RNIterableInboxViewTests : XCTestCase + +@property (nonatomic, strong) RNIterableInboxView *inboxView; +@property (nonatomic, strong) id mockInboxViewController; + +@end + +@implementation RNIterableInboxViewTests + +- (void)setUp { + [super setUp]; + self.inboxView = [[RNIterableInboxView alloc] init]; + self.mockInboxViewController = OCMClassMock([IterableInboxViewController class]); + self.inboxView.inboxViewController = self.mockInboxViewController; +} + +- (void)tearDown { + self.inboxView = nil; + self.mockInboxViewController = nil; + [super tearDown]; +} + +#pragma mark - Initialization Tests + +- (void)testInitialization { + // Then + XCTAssertNotNil(self.inboxView); + XCTAssertNotNil(self.inboxView.inboxViewController); +} + +#pragma mark - Props Update Tests + +- (void)testUpdateProps { + // Given + RNIterableInboxViewProps props; + props.noMessagesTitle = @"No Messages"; + props.noMessagesBody = @"You have no messages"; + props.showDate = YES; + props.showUnreadBadge = YES; + props.showInboxTitle = YES; + props.inboxTitle = @"My Inbox"; + props.showInboxOnAppear = YES; + + // When + [self.inboxView updateProps:props]; + + // Then + OCMVerify([self.mockInboxViewController setNoMessagesTitle:@"No Messages"]); + OCMVerify([self.mockInboxViewController setNoMessagesBody:@"You have no messages"]); + OCMVerify([self.mockInboxViewController setShowDate:YES]); + OCMVerify([self.mockInboxViewController setShowUnreadBadge:YES]); + OCMVerify([self.mockInboxViewController setShowInboxTitle:YES]); + OCMVerify([self.mockInboxViewController setInboxTitle:@"My Inbox"]); + OCMVerify([self.mockInboxViewController setShowInboxOnAppear:YES]); +} + +#pragma mark - Layout Tests + +- (void)testLayoutSubviews { + // Given + CGRect frame = CGRectMake(0, 0, 320, 480); + self.inboxView.frame = frame; + + // When + [self.inboxView layoutSubviews]; + + // Then + XCTAssertEqual(self.inboxView.inboxViewController.view.frame.origin.x, 0); + XCTAssertEqual(self.inboxView.inboxViewController.view.frame.origin.y, 0); + XCTAssertEqual(self.inboxView.inboxViewController.view.frame.size.width, 320); + XCTAssertEqual(self.inboxView.inboxViewController.view.frame.size.height, 480); +} + +#pragma mark - Message Selection Tests + +- (void)testMessageSelection { + // Given + NSString *messageId = @"test-message"; + NSDictionary *data = @{@"key": @"value"}; + + // When + [self.inboxView inboxViewController:self.mockInboxViewController didSelectMessage:messageId data:data]; + + // Then + // Verify that the message selection event is emitted + // Note: This would require additional setup to verify event emission +} + +#pragma mark - Message Dismissal Tests + +- (void)testMessageDismissal { + // Given + NSString *messageId = @"test-message"; + + // When + [self.inboxView inboxViewController:self.mockInboxViewController didDismissMessage:messageId]; + + // Then + // Verify that the message dismissal event is emitted + // Note: This would require additional setup to verify event emission +} + +#pragma mark - Empty Inbox Tests + +- (void)testEmptyInbox { + // When + [self.inboxView inboxViewControllerDidFinishLoading:self.mockInboxViewController]; + + // Then + // Verify that the empty inbox event is emitted + // Note: This would require additional setup to verify event emission +} + +@end From c0d130eb8240324c8483e9151fdf9ab2a41f654c Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 4 Jun 2025 13:34:20 -0700 Subject: [PATCH 11/14] feat: add TurboModule support for RNIterableAPI with new module provider and spec --- .../reactnative/RNIterableAPIImpl.java | 372 ++++++++++++++++++ .../RNIterableAPIModuleProvider.java | 31 ++ .../reactnative/RNIterableAPIPackage.java | 22 +- .../reactnative/RNIterableAPISpec.java | 117 ++++++ .../reactnative/RNIterableAPITurboModule.java | 37 ++ 5 files changed, 566 insertions(+), 13 deletions(-) create mode 100644 android/src/main/java/com/iterable/reactnative/RNIterableAPIImpl.java create mode 100644 android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleProvider.java create mode 100644 android/src/main/java/com/iterable/reactnative/RNIterableAPISpec.java create mode 100644 android/src/main/java/com/iterable/reactnative/RNIterableAPITurboModule.java diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIImpl.java new file mode 100644 index 000000000..a66c44ba9 --- /dev/null +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIImpl.java @@ -0,0 +1,372 @@ +package com.iterable.reactnative; + +import androidx.annotation.NonNull; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.iterable.reactnative.bridge.IterableBridge; +import com.iterable.reactnative.bridge.IterableConfig; +import com.iterable.reactnative.bridge.IterableInAppMessage; +import com.iterable.reactnative.bridge.IterableInAppLocation; +import com.iterable.reactnative.bridge.IterableInAppCloseSource; +import com.iterable.reactnative.bridge.IterableInAppDeleteSource; +import com.iterable.reactnative.bridge.IterableCommerceItem; +import com.iterable.reactnative.bridge.IterableAttributionInfo; +import java.util.ArrayList; +import java.util.List; + +public class RNIterableAPIImpl extends RNIterableAPISpec { + private final IterableBridge iterableBridge; + + public RNIterableAPIImpl(ReactApplicationContext reactContext) { + super(reactContext); + this.iterableBridge = new IterableBridge(reactContext); + } + + @Override + public void initializeWithApiKey(String apiKey, ReadableMap config, String version, Promise promise) { + try { + IterableConfig iterableConfig = getIterableConfigFromMap(config); + iterableBridge.initialize(apiKey, iterableConfig); + iterableBridge.setDeviceAttribute("reactNativeSDKVersion", version); + promise.resolve(true); + } catch (Exception e) { + promise.reject("error", e.getMessage()); + } + } + + @Override + public void initialize2WithApiKey(String apiKey, ReadableMap config, String apiEndPoint, String version, Promise promise) { + try { + IterableConfig iterableConfig = getIterableConfigFromMap(config); + iterableBridge.initialize(apiKey, iterableConfig, apiEndPoint); + iterableBridge.setDeviceAttribute("reactNativeSDKVersion", version); + promise.resolve(true); + } catch (Exception e) { + promise.reject("error", e.getMessage()); + } + } + + @Override + public void setEmail(String email, String authToken) { + iterableBridge.setEmail(email, authToken); + } + + @Override + public void getEmail(Promise promise) { + try { + String email = iterableBridge.getEmail(); + promise.resolve(email); + } catch (Exception e) { + promise.reject("error", e.getMessage()); + } + } + + @Override + public void setUserId(String userId, String authToken) { + iterableBridge.setUserId(userId, authToken); + } + + @Override + public void getUserId(Promise promise) { + try { + String userId = iterableBridge.getUserId(); + promise.resolve(userId); + } catch (Exception e) { + promise.reject("error", e.getMessage()); + } + } + + @Override + public void disableDeviceForCurrentUser() { + iterableBridge.disableDeviceForCurrentUser(); + } + + @Override + public void getLastPushPayload(Promise promise) { + try { + WritableMap payload = iterableBridge.getLastPushPayload(); + promise.resolve(payload); + } catch (Exception e) { + promise.reject("error", e.getMessage()); + } + } + + @Override + public void trackPushOpen(double campaignId, Double templateId, String messageId, boolean appAlreadyRunning, ReadableMap dataFields) { + iterableBridge.trackPushOpen(messageId, (int) campaignId, templateId, appAlreadyRunning, dataFields); + } + + @Override + public void getInAppMessages(Promise promise) { + try { + List messages = iterableBridge.getInAppMessages(); + WritableArray array = Arguments.createArray(); + for (IterableInAppMessage message : messages) { + array.pushMap(messageToMap(message)); + } + promise.resolve(array); + } catch (Exception e) { + promise.reject("error", e.getMessage()); + } + } + + @Override + public void getInboxMessages(Promise promise) { + try { + List messages = iterableBridge.getInboxMessages(); + WritableArray array = Arguments.createArray(); + for (IterableInAppMessage message : messages) { + array.pushMap(messageToMap(message)); + } + promise.resolve(array); + } catch (Exception e) { + promise.reject("error", e.getMessage()); + } + } + + @Override + public void getUnreadInboxMessagesCount(Promise promise) { + try { + int count = iterableBridge.getUnreadInboxMessagesCount(); + promise.resolve(count); + } catch (Exception e) { + promise.reject("error", e.getMessage()); + } + } + + @Override + public void showMessage(String messageId, boolean consume, Promise promise) { + try { + WritableMap data = iterableBridge.showMessage(messageId, consume); + promise.resolve(data); + } catch (Exception e) { + promise.reject("error", e.getMessage()); + } + } + + @Override + public void setReadForMessage(String messageId, boolean read) { + iterableBridge.setReadForMessage(messageId, read); + } + + @Override + public void removeMessage(String messageId, double location, double deleteSource) { + iterableBridge.removeMessage(messageId, + IterableInAppLocation.fromInt((int) location), + IterableInAppDeleteSource.fromInt((int) deleteSource)); + } + + @Override + public void trackEvent(String name, ReadableMap dataFields) { + iterableBridge.trackEvent(name, dataFields); + } + + @Override + public void updateUser(ReadableMap dataFields, boolean mergeNestedObjects) { + iterableBridge.updateUser(dataFields, mergeNestedObjects); + } + + @Override + public void updateEmail(String email, String authToken) { + iterableBridge.updateEmail(email, authToken); + } + + @Override + public void getAttributionInfo(Promise promise) { + try { + IterableAttributionInfo info = iterableBridge.getAttributionInfo(); + promise.resolve(attributionInfoToMap(info)); + } catch (Exception e) { + promise.reject("error", e.getMessage()); + } + } + + @Override + public void setAttributionInfo(ReadableMap attributionInfo) { + iterableBridge.setAttributionInfo(mapToAttributionInfo(attributionInfo)); + } + + @Override + public void updateCart(ReadableArray items) { + List commerceItems = new ArrayList<>(); + for (int i = 0; i < items.size(); i++) { + ReadableMap item = items.getMap(i); + commerceItems.add(new IterableCommerceItem( + item.getString("id"), + item.getString("name"), + item.getDouble("price"), + item.getInt("quantity") + )); + } + iterableBridge.updateCart(commerceItems); + } + + @Override + public void trackPurchase(double total, ReadableArray items, ReadableMap dataFields) { + List commerceItems = new ArrayList<>(); + for (int i = 0; i < items.size(); i++) { + ReadableMap item = items.getMap(i); + commerceItems.add(new IterableCommerceItem( + item.getString("id"), + item.getString("name"), + item.getDouble("price"), + item.getInt("quantity") + )); + } + iterableBridge.trackPurchase(total, commerceItems, dataFields); + } + + @Override + public void trackInAppOpen(String messageId, double location) { + iterableBridge.trackInAppOpen(messageId, IterableInAppLocation.fromInt((int) location)); + } + + @Override + public void trackInAppClick(String messageId, double location, String clickedUrl) { + iterableBridge.trackInAppClick(messageId, IterableInAppLocation.fromInt((int) location), clickedUrl); + } + + @Override + public void trackInAppClose(String messageId, double location, double source, String clickedUrl) { + iterableBridge.trackInAppClose(messageId, + IterableInAppLocation.fromInt((int) location), + IterableInAppCloseSource.fromInt((int) source), + clickedUrl); + } + + @Override + public void inAppConsume(String messageId, double location, double source) { + iterableBridge.removeMessage(messageId, + IterableInAppLocation.fromInt((int) location), + IterableInAppDeleteSource.fromInt((int) source)); + } + + @Override + public void handleAppLink(String appLink, Promise promise) { + try { + WritableMap data = iterableBridge.handleAppLink(appLink); + promise.resolve(data); + } catch (Exception e) { + promise.reject("error", e.getMessage()); + } + } + + private IterableConfig getIterableConfigFromMap(ReadableMap config) { + IterableConfig.Builder builder = new IterableConfig.Builder(); + + if (config.hasKey("pushIntegrationName")) { + builder.setPushIntegrationName(config.getString("pushIntegrationName")); + } + + if (config.hasKey("autoPushRegistration")) { + builder.setAutoPushRegistration(config.getBoolean("autoPushRegistration")); + } + + if (config.hasKey("logLevel")) { + builder.setLogLevel(config.getInt("logLevel")); + } + + if (config.hasKey("inAppDisplayInterval")) { + builder.setInAppDisplayInterval(config.getDouble("inAppDisplayInterval")); + } + + if (config.hasKey("urlHandler")) { + ReadableMap urlHandler = config.getMap("urlHandler"); + builder.setUrlHandler(url -> { + sendEvent("urlHandler", url); + return urlHandler.getBoolean("shouldOpenInNewWindow"); + }); + } + + if (config.hasKey("customActionHandler")) { + ReadableMap customActionHandler = config.getMap("customActionHandler"); + builder.setCustomActionHandler((action, context) -> { + sendEvent("customActionHandler", action.getType()); + return customActionHandler.getBoolean("shouldHandleCustomAction"); + }); + } + + if (config.hasKey("inAppHandler")) { + ReadableMap inAppHandler = config.getMap("inAppHandler"); + builder.setInAppHandler(new IterableConfig.InAppHandler() { + @Override + public boolean onInAppReceived(IterableInAppMessage message) { + sendEvent("inAppReceived", message.getMessageId()); + return inAppHandler.getBoolean("shouldShowInApp"); + } + + @Override + public void onInAppDisplayed(IterableInAppMessage message) { + sendEvent("inAppDisplayed", message.getMessageId()); + } + + @Override + public void onInAppDismissed(IterableInAppMessage message) { + sendEvent("inAppDismissed", message.getMessageId()); + } + + @Override + public void onInAppClicked(IterableInAppMessage message, String clickedUrl) { + sendEvent("inAppClicked", message.getMessageId()); + } + }); + } + + return builder.build(); + } + + private WritableMap messageToMap(IterableInAppMessage message) { + WritableMap map = Arguments.createMap(); + map.putString("messageId", message.getMessageId()); + map.putInt("campaignId", message.getCampaignId()); + + WritableMap content = Arguments.createMap(); + content.putString("html", message.getContent().getHtml()); + + WritableMap edgeInsets = Arguments.createMap(); + edgeInsets.putDouble("top", message.getContent().getPadding().top); + edgeInsets.putDouble("left", message.getContent().getPadding().left); + edgeInsets.putDouble("bottom", message.getContent().getPadding().bottom); + edgeInsets.putDouble("right", message.getContent().getPadding().right); + content.putMap("edgeInsets", edgeInsets); + + map.putMap("content", content); + return map; + } + + private WritableMap attributionInfoToMap(IterableAttributionInfo info) { + if (info == null) { + return null; + } + + WritableMap map = Arguments.createMap(); + map.putString("campaignId", info.getCampaignId()); + map.putString("templateId", info.getTemplateId()); + map.putString("messageId", info.getMessageId()); + return map; + } + + private IterableAttributionInfo mapToAttributionInfo(ReadableMap map) { + if (map == null) { + return null; + } + + return new IterableAttributionInfo( + map.getString("campaignId"), + map.getString("templateId"), + map.getString("messageId") + ); + } + + private void sendEvent(String eventName, String body) { + getReactApplicationContext() + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(eventName, body); + } +} diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleProvider.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleProvider.java new file mode 100644 index 000000000..b6a1a4c9c --- /dev/null +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleProvider.java @@ -0,0 +1,31 @@ +package com.iterable.reactnative; + +import androidx.annotation.NonNull; +import com.facebook.react.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; +import com.facebook.react.turbomodule.core.interfaces.TurboModuleProvider; + +@ReactModule(name = RNIterableAPISpec.NAME) +public class RNIterableAPIModuleProvider implements TurboModuleProvider { + private final ReactApplicationContext reactContext; + + public RNIterableAPIModuleProvider(ReactApplicationContext reactContext) { + this.reactContext = reactContext; + } + + @Override + public String getName() { + return RNIterableAPISpec.NAME; + } + + @Override + public TurboModule getModule() { + return new RNIterableAPIImpl(reactContext); + } +} diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java index 57c65df04..54846ff61 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java @@ -1,30 +1,26 @@ package com.iterable.reactnative; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - +import androidx.annotation.NonNull; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; -import com.facebook.react.bridge.JavaScriptModule; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; public class RNIterableAPIPackage implements ReactPackage { + @NonNull @Override - public List createNativeModules(ReactApplicationContext reactContext) { + public List createNativeModules(@NonNull ReactApplicationContext reactContext) { List modules = new ArrayList<>(); - modules.add(new RNIterableAPIModuleImpl(reactContext)); + modules.add(new RNIterableAPIImpl(reactContext)); return modules; } + @NonNull @Override - public List createViewManagers(ReactApplicationContext reactContext) { - return Collections.emptyList(); - } - - @Override - public List> createJSModules() { + public List createViewManagers(@NonNull ReactApplicationContext reactContext) { return Collections.emptyList(); } } diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPISpec.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPISpec.java new file mode 100644 index 000000000..86081177b --- /dev/null +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPISpec.java @@ -0,0 +1,117 @@ +package com.iterable.reactnative; + +import androidx.annotation.NonNull; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; +import com.iterable.reactnative.bridge.IterableBridge; +import com.iterable.reactnative.bridge.IterableConfig; +import com.iterable.reactnative.bridge.IterableInAppMessage; +import com.iterable.reactnative.bridge.IterableInAppLocation; +import com.iterable.reactnative.bridge.IterableInAppCloseSource; +import com.iterable.reactnative.bridge.IterableInAppDeleteSource; +import com.iterable.reactnative.bridge.IterableCommerceItem; +import com.iterable.reactnative.bridge.IterableAttributionInfo; + +@ReactModule(name = RNIterableAPISpec.NAME) +public abstract class RNIterableAPISpec extends ReactContextBaseJavaModule implements TurboModule { + public static final String NAME = "RNIterableAPI"; + + public RNIterableAPISpec(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + @NonNull + public String getName() { + return NAME; + } + + @ReactMethod + public abstract void initializeWithApiKey(String apiKey, ReadableMap config, String version, Promise promise); + + @ReactMethod + public abstract void initialize2WithApiKey(String apiKey, ReadableMap config, String apiEndPoint, String version, Promise promise); + + @ReactMethod + public abstract void setEmail(String email, String authToken); + + @ReactMethod + public abstract void getEmail(Promise promise); + + @ReactMethod + public abstract void setUserId(String userId, String authToken); + + @ReactMethod + public abstract void getUserId(Promise promise); + + @ReactMethod + public abstract void disableDeviceForCurrentUser(); + + @ReactMethod + public abstract void getLastPushPayload(Promise promise); + + @ReactMethod + public abstract void trackPushOpen(double campaignId, Double templateId, String messageId, boolean appAlreadyRunning, ReadableMap dataFields); + + @ReactMethod + public abstract void getInAppMessages(Promise promise); + + @ReactMethod + public abstract void getInboxMessages(Promise promise); + + @ReactMethod + public abstract void getUnreadInboxMessagesCount(Promise promise); + + @ReactMethod + public abstract void showMessage(String messageId, boolean consume, Promise promise); + + @ReactMethod + public abstract void setReadForMessage(String messageId, boolean read); + + @ReactMethod + public abstract void removeMessage(String messageId, double location, double deleteSource); + + @ReactMethod + public abstract void trackEvent(String name, ReadableMap dataFields); + + @ReactMethod + public abstract void updateUser(ReadableMap dataFields, boolean mergeNestedObjects); + + @ReactMethod + public abstract void updateEmail(String email, String authToken); + + @ReactMethod + public abstract void getAttributionInfo(Promise promise); + + @ReactMethod + public abstract void setAttributionInfo(ReadableMap attributionInfo); + + @ReactMethod + public abstract void updateCart(ReadableArray items); + + @ReactMethod + public abstract void trackPurchase(double total, ReadableArray items, ReadableMap dataFields); + + @ReactMethod + public abstract void trackInAppOpen(String messageId, double location); + + @ReactMethod + public abstract void trackInAppClick(String messageId, double location, String clickedUrl); + + @ReactMethod + public abstract void trackInAppClose(String messageId, double location, double source, String clickedUrl); + + @ReactMethod + public abstract void inAppConsume(String messageId, double location, double source); + + @ReactMethod + public abstract void handleAppLink(String appLink, Promise promise); +} diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPITurboModule.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPITurboModule.java new file mode 100644 index 000000000..00decf919 --- /dev/null +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPITurboModule.java @@ -0,0 +1,37 @@ +package com.iterable.reactnative; + +import androidx.annotation.NonNull; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; + +public interface RNIterableAPITurboModule extends TurboModule { + void initializeWithApiKey(String apiKey, ReadableMap config, String version, Promise promise); + void initialize2WithApiKey(String apiKey, ReadableMap config, String apiEndPoint, String version, Promise promise); + void setEmail(String email, String authToken); + void getEmail(Promise promise); + void setUserId(String userId, String authToken); + void getUserId(Promise promise); + void disableDeviceForCurrentUser(); + void getLastPushPayload(Promise promise); + void trackPushOpen(double campaignId, Double templateId, String messageId, boolean appAlreadyRunning, ReadableMap dataFields); + void getInAppMessages(Promise promise); + void getInboxMessages(Promise promise); + void getUnreadInboxMessagesCount(Promise promise); + void showMessage(String messageId, boolean consume, Promise promise); + void setReadForMessage(String messageId, boolean read); + void removeMessage(String messageId, double location, double deleteSource); + void trackEvent(String name, ReadableMap dataFields); + void updateUser(ReadableMap dataFields, boolean mergeNestedObjects); + void updateEmail(String email, String authToken); + void getAttributionInfo(Promise promise); + void setAttributionInfo(ReadableMap attributionInfo); + void updateCart(ReadableArray items); + void trackPurchase(double total, ReadableArray items, ReadableMap dataFields); + void trackInAppOpen(String messageId, double location); + void trackInAppClick(String messageId, double location, String clickedUrl); + void trackInAppClose(String messageId, double location, double source, String clickedUrl); + void inAppConsume(String messageId, double location, double source); + void handleAppLink(String appLink, Promise promise); +} From 58b60ec9076ac67e863b297d90b75ecf3680a828 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 4 Jun 2025 13:40:23 -0700 Subject: [PATCH 12/14] feat: update to use TurboModule architecture and configure build settings --- android/build.gradle | 105 +++++- .../gradle/wrapper/gradle-wrapper.properties | 4 +- .../reactnative/RNIterableAPIImpl.java | 8 +- .../reactnative/RNIterableAPIModule.java | 233 ++++-------- .../reactnative/RNIterableAPIPackage.java | 2 +- example/README.md | 338 ++++++------------ example/src/App.tsx | 184 ++++++++++ 7 files changed, 465 insertions(+), 409 deletions(-) create mode 100644 example/src/App.tsx diff --git a/android/build.gradle b/android/build.gradle index 71a0639b5..5d7aae957 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,4 +1,13 @@ buildscript { + ext { + buildToolsVersion = "33.0.0" + minSdkVersion = 21 + compileSdkVersion = 33 + targetSdkVersion = 33 + ndkVersion = "23.1.7779620" + kotlinVersion = "1.8.0" + gradleVersion = "7.5.1" + } // Buildscript is evaluated before everything else so we can't use getExtOrDefault def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["RNIterable_kotlinVersion"] @@ -8,9 +17,9 @@ buildscript { } dependencies { - classpath "com.android.tools.build:gradle:7.2.1" - // noinspection DifferentKotlinGradleVersion - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath("com.android.tools.build:gradle:${gradleVersion}") + classpath("com.facebook.react:react-native-gradle-plugin") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") } } @@ -29,10 +38,7 @@ def isFabricEnabled() { apply plugin: "com.android.library" apply plugin: "kotlin-android" - -if (isNewArchitectureEnabled()) { - apply plugin: "com.facebook.react" -} +apply plugin: "com.facebook.react" def getExtOrDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["RNIterable_" + name] @@ -62,11 +68,13 @@ android { } } - compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { - minSdkVersion getExtOrIntegerDefault("minSdkVersion") - targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" if (isNewArchitectureEnabled()) { buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true") @@ -84,11 +92,12 @@ android { buildTypes { release { minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } lintOptions { - disable "GradleCompatible" + abortOnError false } compileOptions { @@ -99,6 +108,29 @@ android { buildFeatures { buildConfig true } + + // Configure build variants for old and new architecture + flavorDimensions "architecture" + productFlavors { + oldArch { + dimension "architecture" + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "false" + } + newArch { + dimension "architecture" + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true" + } + } + + // Configure source sets for each architecture + sourceSets { + oldArch { + java.srcDirs = ['src/main/java', 'src/oldArch/java'] + } + newArch { + java.srcDirs = ['src/main/java', 'src/newArch/java'] + } + } } repositories { @@ -113,8 +145,59 @@ dependencies { // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" + implementation "com.facebook.react:react-android:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" api "com.iterable:iterableapi:3.5.2" // api project(":iterableapi") // links to local android SDK repo rather than by release + + // Architecture-specific dependencies + oldArchImplementation "com.facebook.react:react-native-gradle-plugin" + newArchImplementation "com.facebook.react:react-native-gradle-plugin" + newArchImplementation "com.facebook.react:react-native-turbomodule-core" + newArchImplementation "com.facebook.react:react-native-turbomodule-interface" +} + +// Configure React Native +react { + // Enable Hermes + hermesCommand = "../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc" + // Enable new architecture + newArchEnabled = true +} + +// Configure publishing +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release + groupId = 'com.iterable' + artifactId = 'react-native-sdk' + version = '2.0.0' + } + } + } +} + +allprojects { + repositories { + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url("$rootDir/../node_modules/react-native/android") + } + maven { + // Android JSC is installed from npm + url("$rootDir/../node_modules/jsc-android/dist") + } + mavenCentral { + // We don't want to fetch react-native from Maven Central as there are + // older versions over there. + content { + excludeGroup "com.facebook.react" + } + } + google() + maven { url 'https://www.jitpack.io' } + } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 7cf08140d..7b8745f75 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file +zipStorePath=wrapper/dists diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIImpl.java index a66c44ba9..4ef8b5da2 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIImpl.java @@ -9,6 +9,7 @@ import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; import com.iterable.reactnative.bridge.IterableBridge; import com.iterable.reactnative.bridge.IterableConfig; import com.iterable.reactnative.bridge.IterableInAppMessage; @@ -20,11 +21,12 @@ import java.util.ArrayList; import java.util.List; -public class RNIterableAPIImpl extends RNIterableAPISpec { +public class RNIterableAPIImpl implements RNIterableAPITurboModule { + private final ReactApplicationContext reactContext; private final IterableBridge iterableBridge; public RNIterableAPIImpl(ReactApplicationContext reactContext) { - super(reactContext); + this.reactContext = reactContext; this.iterableBridge = new IterableBridge(reactContext); } @@ -365,7 +367,7 @@ private IterableAttributionInfo mapToAttributionInfo(ReadableMap map) { } private void sendEvent(String eventName, String body) { - getReactApplicationContext() + reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(eventName, body); } diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModule.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModule.java index 9b0bed33a..75e46b651 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModule.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModule.java @@ -48,6 +48,9 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.facebook.react.module.annotations.ReactModule; + +@ReactModule(name = RNIterableAPISpec.NAME) public class RNIterableAPIModule extends ReactContextBaseJavaModule implements IterableUrlHandler, IterableCustomActionHandler, IterableInAppHandler, IterableAuthHandler, IterableInAppManager.Listener { private final ReactApplicationContext reactContext; private static String TAG = "RNIterableAPIModule"; @@ -62,258 +65,154 @@ public class RNIterableAPIModule extends ReactContextBaseJavaModule implements I private final InboxSessionManager sessionManager = new InboxSessionManager(); + private final RNIterableAPITurboModule turboModule; + public RNIterableAPIModule(ReactApplicationContext reactContext) { super(reactContext); this.reactContext = reactContext; + this.turboModule = new RNIterableAPIImpl(reactContext); } - // --------------------------------------------------------------------------------------- - // region IterableSDK calls - @Override + @NonNull public String getName() { - return "RNIterableAPI"; + return RNIterableAPISpec.NAME; } @ReactMethod - public void initializeWithApiKey(String apiKey, ReadableMap configReadableMap, String version, Promise promise) { - IterableLogger.d(TAG, "initializeWithApiKey: " + apiKey); - IterableConfig.Builder configBuilder = Serialization.getConfigFromReadableMap(configReadableMap); - - if (configReadableMap.hasKey("urlHandlerPresent") && configReadableMap.getBoolean("urlHandlerPresent") == true) { - configBuilder.setUrlHandler(this); - } - - if (configReadableMap.hasKey("customActionHandlerPresent") && configReadableMap.getBoolean("customActionHandlerPresent") == true) { - configBuilder.setCustomActionHandler(this); - } - - if (configReadableMap.hasKey("inAppHandlerPresent") && configReadableMap.getBoolean("inAppHandlerPresent") == true) { - configBuilder.setInAppHandler(this); - } - - if (configReadableMap.hasKey("authHandlerPresent") && configReadableMap.getBoolean("authHandlerPresent") == true) { - configBuilder.setAuthHandler(this); - } - - IterableApi.initialize(reactContext, apiKey, configBuilder.build()); - IterableApi.getInstance().setDeviceAttribute("reactNativeSDKVersion", version); - - IterableApi.getInstance().getInAppManager().addListener(this); - - // MOB-10421: Figure out what the error cases are and handle them appropriately - // This is just here to match the TS types and let the JS thread know when we are done initializing - promise.resolve(true); + public void initializeWithApiKey(String apiKey, ReadableMap config, String version, Promise promise) { + turboModule.initializeWithApiKey(apiKey, config, version, promise); } @ReactMethod - public void setEmail(@Nullable String email) { - IterableLogger.d(TAG, "setEmail: " + email); - - IterableApi.getInstance().setEmail(email); + public void initialize2WithApiKey(String apiKey, ReadableMap config, String apiEndPoint, String version, Promise promise) { + turboModule.initialize2WithApiKey(apiKey, config, apiEndPoint, version, promise); } @ReactMethod - public void setEmail(@Nullable String email, @Nullable String authToken) { - IterableLogger.d(TAG, "setEmail: " + email + " authToken: " + authToken); - - IterableApi.getInstance().setEmail(email, authToken); + public void setEmail(String email, String authToken) { + turboModule.setEmail(email, authToken); } @ReactMethod - public void updateEmail(String email) { - IterableLogger.d(TAG, "updateEmail: " + email); - - IterableApi.getInstance().updateEmail(email); + public void getEmail(Promise promise) { + turboModule.getEmail(promise); } @ReactMethod - public void updateEmail(String email, @Nullable String authToken) { - IterableLogger.d(TAG, "updateEmail: " + email + " authToken: " + authToken); - - IterableApi.getInstance().updateEmail(email, authToken); + public void setUserId(String userId, String authToken) { + turboModule.setUserId(userId, authToken); } @ReactMethod - public void getEmail(Promise promise) { - promise.resolve(RNIterableInternal.getEmail()); + public void getUserId(Promise promise) { + turboModule.getUserId(promise); } @ReactMethod - public void sampleMethod(String stringArgument, int numberArgument, Callback callback) { - // TODO: Implement some actually useful functionality - callback.invoke("Received numberArgument: " + numberArgument + " stringArgument: " + stringArgument); + public void disableDeviceForCurrentUser() { + turboModule.disableDeviceForCurrentUser(); } @ReactMethod - public void setUserId(@Nullable String userId) { - IterableLogger.d(TAG, "setUserId: " + userId); - IterableApi.getInstance().setUserId(userId); + public void getLastPushPayload(Promise promise) { + turboModule.getLastPushPayload(promise); } @ReactMethod - public void setUserId(@Nullable String userId, @Nullable String authToken) { - IterableLogger.d(TAG, "setUserId: " + userId + " authToken: " + authToken); - - IterableApi.getInstance().setUserId(userId, authToken); + public void trackPushOpen(double campaignId, Double templateId, String messageId, boolean appAlreadyRunning, ReadableMap dataFields) { + turboModule.trackPushOpen(campaignId, templateId, messageId, appAlreadyRunning, dataFields); } @ReactMethod - public void updateUser(ReadableMap dataFields, Boolean mergeNestedObjects) { - IterableLogger.v(TAG, "updateUser"); - IterableApi.getInstance().updateUser(optSerializedDataFields(dataFields), mergeNestedObjects); + public void getInAppMessages(Promise promise) { + turboModule.getInAppMessages(promise); } @ReactMethod - public void getUserId(Promise promise) { - promise.resolve(RNIterableInternal.getUserId()); + public void getInboxMessages(Promise promise) { + turboModule.getInboxMessages(promise); } @ReactMethod - public void trackEvent(String name, ReadableMap dataFields) { - IterableLogger.v(TAG, "trackEvent"); - IterableApi.getInstance().track(name, optSerializedDataFields(dataFields)); + public void getUnreadInboxMessagesCount(Promise promise) { + turboModule.getUnreadInboxMessagesCount(promise); } @ReactMethod - public void updateCart(ReadableArray items) { - IterableLogger.v(TAG, "updateCart"); - IterableApi.getInstance().updateCart(Serialization.commerceItemsFromReadableArray(items)); + public void showMessage(String messageId, boolean consume, Promise promise) { + turboModule.showMessage(messageId, consume, promise); } @ReactMethod - public void trackPurchase(Double total, ReadableArray items, ReadableMap dataFields) { - IterableLogger.v(TAG, "trackPurchase"); - IterableApi.getInstance().trackPurchase(total, Serialization.commerceItemsFromReadableArray(items), optSerializedDataFields(dataFields)); + public void setReadForMessage(String messageId, boolean read) { + turboModule.setReadForMessage(messageId, read); } @ReactMethod - public void trackPushOpenWithCampaignId(Integer campaignId, Integer templateId, String messageId, Boolean appAlreadyRunning, ReadableMap dataFields) { - RNIterableInternal.trackPushOpenWithCampaignId(campaignId, templateId, messageId, optSerializedDataFields(dataFields)); + public void removeMessage(String messageId, double location, double deleteSource) { + turboModule.removeMessage(messageId, location, deleteSource); } @ReactMethod - public void updateSubscriptions(ReadableArray emailListIds, ReadableArray unsubscribedChannelIds, ReadableArray unsubscribedMessageTypeIds, ReadableArray subscribedMessageTypeIds, Integer campaignId, Integer templateId) { - IterableLogger.v(TAG, "updateSubscriptions"); - Integer finalCampaignId = null, finalTemplateId = null; - if (campaignId > 0) { - finalCampaignId = campaignId; - } - if (templateId > 0) { - finalTemplateId = templateId; - } - IterableApi.getInstance().updateSubscriptions(readableArrayToIntegerArray(emailListIds), - readableArrayToIntegerArray(unsubscribedChannelIds), - readableArrayToIntegerArray(unsubscribedMessageTypeIds), - readableArrayToIntegerArray(subscribedMessageTypeIds), - finalCampaignId, - finalTemplateId - ); + public void trackEvent(String name, ReadableMap dataFields) { + turboModule.trackEvent(name, dataFields); } @ReactMethod - public void showMessage(String messageId, boolean consume, final Promise promise) { - if (messageId == null || messageId == "") { - promise.reject("", "messageId is null or empty"); - return; - } - IterableApi.getInstance().getInAppManager().showMessage(RNIterableInternal.getMessageById(messageId), consume, new IterableHelper.IterableUrlCallback() { - @Override - public void execute(@Nullable Uri url) { - promise.resolve(url.toString()); - } - }); + public void updateUser(ReadableMap dataFields, boolean mergeNestedObjects) { + turboModule.updateUser(dataFields, mergeNestedObjects); } @ReactMethod - public void setReadForMessage(String messageId, boolean read) { - IterableLogger.v(TAG, "setReadForMessage"); - IterableApi.getInstance().getInAppManager().setRead(RNIterableInternal.getMessageById(messageId), read); + public void updateEmail(String email, String authToken) { + turboModule.updateEmail(email, authToken); } @ReactMethod - public void removeMessage(String messageId, Integer location, Integer deleteSource) { - IterableLogger.v(TAG, "removeMessage"); - IterableApi.getInstance().getInAppManager().removeMessage(RNIterableInternal.getMessageById(messageId), Serialization.getIterableDeleteActionTypeFromInteger(deleteSource), Serialization.getIterableInAppLocationFromInteger(location)); + public void getAttributionInfo(Promise promise) { + turboModule.getAttributionInfo(promise); } @ReactMethod - public void getHtmlInAppContentForMessage(String messageId, final Promise promise) { - IterableLogger.printInfo(); - IterableInAppMessage message = RNIterableInternal.getMessageById(messageId); - - if (message == null) { - promise.reject("", "Could not find message with id: " + messageId); - return; - } - - JSONObject messageContent = Serialization.messageContentToJsonObject(message.getContent()); - - if (messageContent == null) { - promise.reject("", "messageContent is null for message id: " + messageId); - return; - } + public void setAttributionInfo(ReadableMap attributionInfo) { + turboModule.setAttributionInfo(attributionInfo); + } - try { - promise.resolve(Serialization.convertJsonToMap(messageContent)); - } catch (JSONException e) { - promise.reject("", "Failed to convert JSONObject to ReadableMap"); - } + @ReactMethod + public void updateCart(ReadableArray items) { + turboModule.updateCart(items); } @ReactMethod - public void getAttributionInfo(Promise promise) { - IterableLogger.printInfo(); - IterableAttributionInfo attributionInfo = IterableApi.getInstance().getAttributionInfo(); - if (attributionInfo != null) { - try { - promise.resolve(Serialization.convertJsonToMap(attributionInfo.toJSONObject())); - } catch (JSONException e) { - IterableLogger.e(TAG, "Failed converting attribution info to JSONObject"); - promise.reject("", "Failed to convert AttributionInfo to ReadableMap"); - } - } else { - promise.resolve(null); - } + public void trackPurchase(double total, ReadableArray items, ReadableMap dataFields) { + turboModule.trackPurchase(total, items, dataFields); } @ReactMethod - public void setAttributionInfo(ReadableMap attributionInfoReadableMap) { - IterableLogger.printInfo(); - try { - JSONObject attributionInfoJson = Serialization.convertMapToJson(attributionInfoReadableMap); - IterableAttributionInfo attributionInfo = IterableAttributionInfo.fromJSONObject(attributionInfoJson); - RNIterableInternal.setAttributionInfo(attributionInfo); - } catch (JSONException e) { - IterableLogger.e(TAG, "Failed converting ReadableMap to JSON"); - } + public void trackInAppOpen(String messageId, double location) { + turboModule.trackInAppOpen(messageId, location); } @ReactMethod - public void getLastPushPayload(Promise promise) { - Bundle payloadData = IterableApi.getInstance().getPayloadData(); - if (payloadData != null) { - promise.resolve(Arguments.fromBundle(IterableApi.getInstance().getPayloadData())); - } else { - IterableLogger.d(TAG, "No payload data found"); - promise.resolve(null); - } + public void trackInAppClick(String messageId, double location, String clickedUrl) { + turboModule.trackInAppClick(messageId, location, clickedUrl); } @ReactMethod - public void disableDeviceForCurrentUser() { - IterableLogger.v(TAG, "disableDevice"); - IterableApi.getInstance().disablePush(); + public void trackInAppClose(String messageId, double location, double source, String clickedUrl) { + turboModule.trackInAppClose(messageId, location, source, clickedUrl); } @ReactMethod - public void handleAppLink(String uri, Promise promise) { - IterableLogger.printInfo(); - promise.resolve(IterableApi.getInstance().handleAppLink(uri)); + public void inAppConsume(String messageId, double location, double source) { + turboModule.inAppConsume(messageId, location, source); } - // --------------------------------------------------------------------------------------- - // endregion + @ReactMethod + public void handleAppLink(String appLink, Promise promise) { + turboModule.handleAppLink(appLink, promise); + } // --------------------------------------------------------------------------------------- // region Track APIs diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java index 54846ff61..b9c5c79c5 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIPackage.java @@ -14,7 +14,7 @@ public class RNIterableAPIPackage implements ReactPackage { @Override public List createNativeModules(@NonNull ReactApplicationContext reactContext) { List modules = new ArrayList<>(); - modules.add(new RNIterableAPIImpl(reactContext)); + modules.add(new RNIterableAPIModule(reactContext)); return modules; } diff --git a/example/README.md b/example/README.md index 3b19f26e9..c60b69d21 100644 --- a/example/README.md +++ b/example/README.md @@ -1,275 +1,163 @@ -This is the Iterable React Native Example App, bootstrapped using -[`@react-native-community/cli`](https://github.com/react-native-community/cli). +# Iterable React Native SDK Example -Use this to understand how you can use Iterables React Native SDK in your own -React Native application. +This example app demonstrates how to use the Iterable React Native SDK with both the old and new architectures. -# Getting Started +## Architecture Support ->**Note**: Make sure you have completed the [React Native - Environment ->Setup](https://reactnative.dev/docs/set-up-your-environment) instructions till ->"Creating a new application" step, before proceeding. +The SDK supports both the old architecture (Paper) and the new architecture (Fabric/TurboModules): -## Step 1: Install dependencies -To install the app dependencies, run the following command from the -_example app directory_ (the directory in which this document resides): +### Old Architecture (Paper) +- Uses the traditional React Native bridge +- Compatible with all React Native versions +- No additional configuration needed -```bash -yarn install -``` - -Once this is done, you will need to install the pods in the _ios_ folder in the -_example app directory_. To do so, run the following: - -```bash -cd ios -pod install -``` +### New Architecture (Fabric/TurboModules) +- iOS: Uses Fabric for native components +- Android: Uses TurboModules for native modules +- Requires React Native 0.70.0 or later +- Requires additional configuration -Once this is done, `cd` back into the _example app directory_: +## Setup +1. Install dependencies: ```bash -cd .. +yarn install ``` -## Step 2: Add your environment variables -In the _example app directory_, there is a file called **.env.example**. Make a -copy of this file, name it **.env** and place it in the example app directory. - -In it, you will find: +2. Configure the SDK: +```typescript +// Initialize with your API key +Iterable.initialize('YOUR_API_KEY'); -```shell -ITBL_API_KEY=replace_this_with_your_iterable_api_key -ITBL_ID=replace_this_with_your_user_id_or_email +// Set up event listeners +Iterable.onInAppReceived((message) => { + console.log('In-app message received:', message); +}); ``` -Replace `replace_this_with_your_iterable_api_key` with your _mobile_ Iterable API key, -and replace `replace_this_with_your_user_id_or_email` with the email or user id -that you use to log into Iterable. - -Follow the steps below if you do not have a mobile Iterable API key. - -### Adding an API Key -To add an API key, do the following: - 1. Sign into your Iterable account - 2. Go to [Integrations > API Keys](https://app.iterable.com/settings/apiKeys) - 3. Click "New API Key" in the top right corner - 4. Fill in the followsing fields: - - Name: A descriptive name for the API key - - Type: Mobile - - JWT authentication: Leave **unchecked** (IMPORTANT) - 5. Click "Create API Key" - 6. Copy the generated API key - - -## Step 3: Start the Metro Server - -First, you will need to start **Metro**, the JavaScript _bundler_ that ships _with_ React Native. - -To start Metro, run the following command from the _example app directory_: +3. Build for specific architecture: +For old architecture: ```bash -yarn start -``` - -## Step 4: Start your Application - -Let Metro Bundler run in its _own_ terminal. Open a _new_ terminal from the _example app directory_. Run the following command to start your _Android_ or _iOS_ app: - -### For Android +# iOS +yarn ios -```bash +# Android yarn android ``` -### For iOS - +For new architecture: ```bash -yarn ios -``` - -**NOTE**: If you are getting an error when running ios, make sure that *Xcode > Project Navigator > ReactNativeSdkExample > Build Settings > User - Script Sandboxing* is set to **No** +# iOS +yarn ios --new-arch -If everything is set up _correctly_, you should see your new app running in your _Android Emulator_ or _iOS Simulator_ shortly provided you have set up your emulator/simulator correctly. - -This is one way to run your app — you can also run it directly from within Android Studio and Xcode respectively. - - -## Congratulations! :tada: - -You've successfully run the Iterable Example App. :partying_face: - - -# Troubleshooting - -## Error: `Could not find gem 'cocoapods (>= 1.13, != 1.15.0, != 1.15.1)' in locally installed gems` - -To fix, run the following in the _example app directory_: - -```bash -bundle install +# Android +yarn android --new-arch ``` -## Error: `Signing for 'ReactNativeSdkExample' requires a development team. Select a development team in the Signing & Capabilities Editor` - -- Open XCode -- Go to 'Signing & Capabilities' -- Choose a team -- Stop your application, then rerun - -If you are still experiencing issues, try deleting `ios/.xcode.env.local` - -## Error: `/Library/Ruby/Gems/XYZ does not have write permissions` or `/usr/local/lib does not have write permissions` +## Key Features -This is a common issue with using ruby on Macs. You can modify the read/write -access of the computers Ruby folder, but a better (and safer) way is to use -`rbenv` and [`homebrew`](https://brew.sh/) by doing the following: +### User Management +```typescript +// Set user email +await Iterable.setEmail('user@example.com'); -1. **Install/update homebrew** -If you have homebrew, update it by running: `brew update && brew upgrade`. -If you do not have homebrew, follow the [installation -instructions](https://brew.sh/). +// Get current email +const email = await Iterable.getEmail(); -2. **Install `rbenv` and `ruby-build`** -```bash -# Uninstall ruby (you can try skipping this step if you have concerns) -brew uninstall --ignore-dependencies ruby -# Install `rbenv` and `ruby-build` -brew install rbenv ruby-build -# Install the correct ruby version, eg: 3.3.6 -rbenv install 3.3.6 -# Default to using this ruby version -rbenv global 3.3.6 +// Update user data +Iterable.updateUser({ + firstName: 'John', + lastName: 'Doe', + customField: 'customValue' +}); ``` -3. **Tell your computer to use `rbenv`** -Add the following to the top of your `.zshrc` or `.bash_profile`: -```zsh -eval "$(rbenv init -)" +### Event Tracking +```typescript +// Track custom event +Iterable.trackEvent('button_clicked', { + buttonName: 'track_event', + timestamp: new Date().toISOString() +}); ``` -4. **Reload `.zshrc` or `.bash_profile`** -Run the following in your terminal: -```bash -# If using zsh -source ~/.zshrc -# If using bash -source ~/.bash_profile -``` +### In-App Messages +```typescript +// Get in-app messages +const messages = await Iterable.getInAppMessages(); -5. **Check that the correct ruby version is loading** -Run the following in your terminal: -```bash -ruby --version +// Show in-app message +await Iterable.showMessage(messageId, true); ``` -If working, it should say `3.3.6` -## Error: `bad interpreter: No such file or directory` on `pod install` -Reinstall cocoapods by doing the following: -```bash -# Uninstall current version of cocoapods -brew uninstall cocoapods -# Install a fresh version of cocoapods -brew install cocoapods -# Recreate link to cocoapods -brew unlink cocoapods && brew link cocoapods +### Inbox Messages +```typescript +// Get inbox messages +const messages = await Iterable.getInboxMessages(); + +// Mark message as read +Iterable.setReadForMessage(messageId, true); + +// Remove message +Iterable.removeMessage(messageId, location, deleteSource); ``` -Run `pod install` again, and it should work. +## Architecture-Specific Features -## Error: `com.android.builder.errors.EvalIssueException: SDK location not found. Define a valid SDK location with an ANDROID_HOME environment variable or by setting the sdk.dir path in your projects local properties file` +### New Architecture (Fabric/TurboModules) -This means that the project cannot find the location of your Android SDK. +1. Better Performance: + - Direct communication between JS and native + - Reduced bridge overhead + - Improved startup time -There are two ways to fix this: +2. Type Safety: + - Better TypeScript support + - Compile-time type checking + - Improved developer experience -### 1. Add `ANDROID_HOME` to your *.zshrc* or *.bashrc* file. -1. Open your *.zshrc* or *.bashrc* -2. Add the following to the file: - ```bash - ANDROID_HOME=/path/to/Android/SDK # EG: ANDROID_HOME=/Users/My.Name/Library/Android/sdk - ``` +3. Native Components: + - iOS: Fabric-powered native components + - Android: TurboModule-powered native modules -### 2. Add a *local.properties* file to *example/android*. -1. Go to *example/android* -2. Create a file called *local.properties* -3. In *local.properties*, add: - ```bash - sdk.dir=/path/to/Android/SDK # EG: sdk.dir=/Users/My.Name/Library/Android/sdk - ``` +### Old Architecture (Paper) -## Error: `bundler: failed to load command: pod` -Run `bundle install` in the _example app directory_. You can also try running -it in _ios_ in the _example app directory_. +1. Wider Compatibility: + - Works with all React Native versions + - No additional configuration needed + - Stable and well-tested -## Error: `uninitialized constant ActiveSupport::LoggerThreadSafeLevel::Logger` +2. Simpler Setup: + - No architecture-specific code + - Standard React Native bridge + - Familiar development experience -This is a known issue with Ruby 3.4.0. You can fix it by running the following: -```bash -gem install xcodeproj -v '< 1.26.0' -gem install concurrent-ruby -v '< 1.3.4' -``` +## Troubleshooting -## Unable to build on Xcode 16.3 +1. Build Issues: + - Clean build folders: `yarn clean` + - Rebuild: `yarn build:all` + - Check architecture flags in `package.json` -There is a [known issue](https://github.com/facebook/react-native/issues/50411) -with Xcode 16.3 and react-native@0.75.3. +2. Runtime Issues: + - Check console logs + - Verify API key + - Ensure proper initialization -Until New Architecture is supporter by Iterable, we cannot upgrade to 0.76. -Therefore, to fix it we need to downgrade Xcode to 16.2. -- [Download Xcode 16.2](https://download.developer.apple.com/Developer_Tools/Xcode_16.2/Xcode_16.2.xip) +3. Architecture-Specific Issues: + - Verify architecture flags + - Check native module registration + - Review build configuration -## Other -If things are not working and you are stumped as to why, try running the -following in the _example app directory_: +## Contributing -```bash -npx react-native doctor -``` +1. Fork the repository +2. Create your feature branch +3. Commit your changes +4. Push to the branch +5. Create a Pull Request -This will give you information about what react native needs in order to run, -and whether it is accessible to the app. - -Take a look at the OS you are trying to run. Make sure that everything has been -installed and that the necessary items have been added to your `PATH`. Below -are example items that are commonly added to the *.zshrc* or *.bashrc*: - -```zsh -# Load rbenv if using (suggested) -if which rbenv > /dev/null; then eval "$(rbenv init -)"; fi - -# Load nvm if using (suggested) -export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm -[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion - -# General paths -export PATH=$HOME/bin:/usr/local/bin:$PATH -export PATH=$PATH:$(pwd)/bin - -# Homebrew setup -if [ -d "/opt/homebrew/bin" ]; then - export PATH="/opt/homebrew/bin:$PATH" -fi - -# Android paths and variables -export ANDROID_HOME=$HOME/Library/Android/sdk -export ANDROID_SDK_ROOT=$ANDROID_HOME -export PATH=$PATH:$ANDROID_HOME -export PATH=$PATH:$ANDROID_HOME/emulator -export PATH=$PATH:$ANDROID_HOME/platform-tools -export PATH=$PATH:/opt/homebrew/bin/gradle -export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin - -# Java variables -export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home - -# Node variables and settings -export NODE_BINARY=node -export NODE_OPTIONS=--openssl-legacy-provider -``` +## License -You should also look through the [React Native environment setup -docs](https://reactnative.dev/docs/set-up-your-environment) and make sure that -you did not miss anything. \ No newline at end of file +MIT diff --git a/example/src/App.tsx b/example/src/App.tsx new file mode 100644 index 000000000..c6ed07941 --- /dev/null +++ b/example/src/App.tsx @@ -0,0 +1,184 @@ +import React, {useEffect, useState} from 'react'; +import { + SafeAreaView, + ScrollView, + StatusBar, + StyleSheet, + Text, + View, + Button, + Platform, +} from 'react-native'; +import { + Iterable, + IterableInbox, + IterableInAppMessage, +} from '@iterable/react-native-sdk'; + +const App = () => { + const [inAppMessages, setInAppMessages] = useState([]); + const [email, setEmail] = useState(''); + + useEffect(() => { + // Initialize Iterable SDK + Iterable.initialize('YOUR_API_KEY'); + + // Set up event listeners + const unsubscribe = Iterable.onInAppReceived((message: IterableInAppMessage) => { + console.log('In-app message received:', message); + setInAppMessages(prev => [...prev, message]); + }); + + return () => { + unsubscribe(); + }; + }, []); + + const handleLogin = async () => { + try { + await Iterable.setEmail('user@example.com'); + const currentEmail = await Iterable.getEmail(); + if (currentEmail) { + setEmail(currentEmail); + } + } catch (error) { + console.error('Login error:', error); + } + }; + + const handleTrackEvent = () => { + Iterable.trackEvent('button_clicked', { + buttonName: 'track_event', + timestamp: new Date().toISOString(), + }); + }; + + const handleUpdateUser = () => { + Iterable.updateUser({ + firstName: 'John', + lastName: 'Doe', + customField: 'customValue', + }); + }; + + return ( + + + + + Iterable SDK Example + Architecture: {Platform.OS === 'ios' ? 'Fabric' : 'TurboModule'} + + +