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