diff --git a/__tests__/android/__snapshots__/app-gradle-build.test.js.snap b/__tests__/android/__snapshots__/app-gradle-build.test.js.snap deleted file mode 100644 index abf39647..00000000 --- a/__tests__/android/__snapshots__/app-gradle-build.test.js.snap +++ /dev/null @@ -1,182 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Plugin applies Google Services plugin in the app Gradle build file 1`] = ` -"apply plugin: "com.android.application" -apply plugin: "com.google.gms.google-services" // Google Services plugin -apply plugin: "org.jetbrains.kotlin.android" -apply plugin: "com.facebook.react" - -def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() - -/** - * This is the configuration block to customize your React Native Android app. - * By default you don't need to apply any configuration, just uncomment the lines you need. - */ -react { - entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim()) - reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() - hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc" - codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() - - // Use Expo CLI to bundle the app, this ensures the Metro config - // works correctly with Expo projects. - cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim()) - bundleCommand = "export:embed" - - /* Folders */ - // The root of your project, i.e. where "package.json" lives. Default is '../..' - // root = file("../../") - // The folder where the react-native NPM package is. Default is ../../node_modules/react-native - // reactNativeDir = file("../../node_modules/react-native") - // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen - // codegenDir = file("../../node_modules/@react-native/codegen") - - /* Variants */ - // The list of variants to that are debuggable. For those we're going to - // skip the bundling of the JS bundle and the assets. By default is just 'debug'. - // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. - // debuggableVariants = ["liteDebug", "prodDebug"] - - /* Bundling */ - // A list containing the node command and its flags. Default is just 'node'. - // nodeExecutableAndArgs = ["node"] - - // - // The path to the CLI configuration file. Default is empty. - // bundleConfig = file(../rn-cli.config.js) - // - // The name of the generated asset file containing your JS bundle - // bundleAssetName = "MyApplication.android.bundle" - // - // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' - // entryFile = file("../js/MyApplication.android.js") - // - // A list of extra flags to pass to the 'bundle' commands. - // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle - // extraPackagerArgs = [] - - /* Hermes Commands */ - // The hermes compiler command to run. By default it is 'hermesc' - // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" - // - // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" - // hermesFlags = ["-O", "-output-source-map"] - - /* Autolinking */ - autolinkLibrariesWithApp() -} - -/** - * Set this to true to Run Proguard on Release builds to minify the Java bytecode. - */ -def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean() - -/** - * The preferred build flavor of JavaScriptCore (JSC) - * - * For example, to use the international variant, you can use: - * \`def jscFlavor = 'org.webkit:android-jsc-intl:+'\` - * - * The international variant includes ICU i18n library and necessary data - * allowing to use e.g. \`Date.toLocaleString\` and \`String.localeCompare\` that - * give correct results when using with locales other than en-US. Note that - * this variant is about 6MiB larger per architecture than default. - */ -def jscFlavor = 'org.webkit:android-jsc:+' - -android { - ndkVersion rootProject.ext.ndkVersion - - buildToolsVersion rootProject.ext.buildToolsVersion - compileSdk rootProject.ext.compileSdkVersion - - namespace 'io.customer.testbed.expo' - defaultConfig { - applicationId 'io.customer.testbed.expo' - minSdkVersion rootProject.ext.minSdkVersion - targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1 - versionName "1.0.0" - } - signingConfigs { - debug { - storeFile file('debug.keystore') - storePassword 'android' - keyAlias 'androiddebugkey' - keyPassword 'android' - } - } - buildTypes { - debug { - signingConfig signingConfigs.debug - } - release { - // Caution! In production, you need to generate your own keystore file. - // see https://reactnative.dev/docs/signed-apk-android. - signingConfig signingConfigs.debug - shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false) - minifyEnabled enableProguardInReleaseBuilds - proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" - crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true) - } - } - packagingOptions { - jniLibs { - useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false) - } - } - androidResources { - ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~' - } -} - -// Apply static values from \`gradle.properties\` to the \`android.packagingOptions\` -// Accepts values in comma delimited lists, example: -// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini -["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop -> - // Split option: 'foo,bar' -> ['foo', 'bar'] - def options = (findProperty("android.packagingOptions.$prop") ?: "").split(","); - // Trim all elements in place. - for (i in 0.. 0) { - println "android.packagingOptions.$prop += $options ($options.length)" - // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**' - options.each { - android.packagingOptions[prop] += it - } - } -} - -dependencies { - // The version of react-native is set by the React Native Gradle Plugin - implementation("com.facebook.react:react-android") - - def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true"; - def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true"; - def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true"; - - if (isGifEnabled) { - // For animated gif support - implementation("com.facebook.fresco:animated-gif:\${reactAndroidLibs.versions.fresco.get()}") - } - - if (isWebpEnabled) { - // For webp support - implementation("com.facebook.fresco:webpsupport:\${reactAndroidLibs.versions.fresco.get()}") - if (isWebpAnimatedEnabled) { - // Animated webp support - implementation("com.facebook.fresco:animated-webp:\${reactAndroidLibs.versions.fresco.get()}") - } - } - - if (hermesEnabled.toBoolean()) { - implementation("com.facebook.react:hermes-android") - } else { - implementation jscFlavor - } -} -" -`; diff --git a/__tests__/android/__snapshots__/app-manifest.test.js.snap b/__tests__/android/__snapshots__/app-manifest.test.js.snap deleted file mode 100644 index f7dfd0bf..00000000 --- a/__tests__/android/__snapshots__/app-manifest.test.js.snap +++ /dev/null @@ -1,46 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Plugin injects CustomerIOFirebaseMessagingService in the app manifest 1`] = ` -" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" -`; diff --git a/__tests__/android/__snapshots__/main-gradle-build.test.js.snap b/__tests__/android/__snapshots__/main-gradle-build.test.js.snap deleted file mode 100644 index 29bf6e75..00000000 --- a/__tests__/android/__snapshots__/main-gradle-build.test.js.snap +++ /dev/null @@ -1,48 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Plugin injects expted dependencies in the main Gradle build file 1`] = ` -"// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - ext { - buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0' - minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24') - compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35') - targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34') - kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25' - - ndkVersion = "26.1.10909125" - } - repositories { - google() - mavenCentral() - } - dependencies { - classpath "com.google.gms:google-services:4.3.13" // Google Services plugin - classpath('com.android.tools.build:gradle') - classpath('com.facebook.react:react-native-gradle-plugin') - classpath('org.jetbrains.kotlin:kotlin-gradle-plugin') - } -} - -apply plugin: "com.facebook.react.rootproject" - -allprojects { - repositories { - maven { url "https://maven.gist.build" } - maven { - // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm - url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android')) - } - maven { - // Android JSC is installed from npm - url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist')) - } - - google() - mavenCentral() - maven { url 'https://www.jitpack.io' } - } -} -" -`; diff --git a/__tests__/android/app-gradle-build.test.js b/__tests__/android/app-gradle-build.test.js index 8934cbe2..41fdf1ca 100644 --- a/__tests__/android/app-gradle-build.test.js +++ b/__tests__/android/app-gradle-build.test.js @@ -1,12 +1,14 @@ -const fs = require("fs-extra"); +const { testAppPath } = require("../utils"); const path = require("path"); +const g2js = require('gradle-to-js/lib/parser'); -const testProjectPath = path.join(__dirname, "../../test-app"); +const testProjectPath = testAppPath(); const androidPath = path.join(testProjectPath, "android"); const appBuildGradlePath = path.join(androidPath, "app/build.gradle"); test("Plugin applies Google Services plugin in the app Gradle build file", async () => { - const content = await fs.readFile(appBuildGradlePath, "utf8"); + const gradleFileAsJson = await g2js.parseFile(appBuildGradlePath); - expect(content).toMatchSnapshot(); + const hasPlugin = gradleFileAsJson.apply.some(plugin => plugin.includes('com.google.gms.google-services')); + expect(hasPlugin).toBe(true); }); diff --git a/__tests__/android/app-manifest.test.js b/__tests__/android/app-manifest.test.js index 6f7c4628..b08cf8f5 100644 --- a/__tests__/android/app-manifest.test.js +++ b/__tests__/android/app-manifest.test.js @@ -1,14 +1,36 @@ +const { testAppPath } = require("../utils"); const fs = require("fs-extra"); const path = require("path"); +const { parseString } = require('xml2js'); -const testProjectPath = path.join(__dirname, "../../test-app"); +const testProjectPath = testAppPath(); const androidPath = path.join(testProjectPath, "android"); const appManifestPath = path.join(androidPath, "app/src/main/AndroidManifest.xml"); test("Plugin injects CustomerIOFirebaseMessagingService in the app manifest", async () => { // When setHighPriorityPushHandler config is set to true when setting up the plugin // an intent filter for CustomerIOFirebaseMessagingService is added to the app Manifest file - const content = await fs.readFile(appManifestPath, "utf8"); + const manifestContent = await fs.readFile(appManifestPath, "utf8"); - expect(content).toMatchSnapshot(); + parseString(manifestContent, (err, manifest) => { + if (err) throw err; + + const expectedServiceName = 'io.customer.messagingpush.CustomerIOFirebaseMessagingService'; + const expectedAction = 'com.google.firebase.MESSAGING_EVENT'; + + const application = manifest?.manifest?.application?.[0]; + expect(application).toBeDefined(); + + const services = application.service || []; + const service = services.find(service => service['$']['android:name'] === expectedServiceName); + expect(service).toBeDefined(); + + expect(service['$']['android:exported']).toBe('false'); + expect(service['intent-filter']).toBeDefined(); + expect(service['intent-filter'].length).toBeGreaterThan(0); + + const actions = service['intent-filter'][0].action || []; + const hasExpectedAction = actions.some(action => action['$']['android:name'] === expectedAction); + expect(hasExpectedAction).toBe(true); + }); }); diff --git a/__tests__/android/main-gradle-build.test.js b/__tests__/android/main-gradle-build.test.js index 1852d496..c5d5949b 100644 --- a/__tests__/android/main-gradle-build.test.js +++ b/__tests__/android/main-gradle-build.test.js @@ -1,12 +1,23 @@ -const fs = require("fs-extra"); -const path = require("path"); +const { testAppPath } = require("../utils"); +const fs = require('fs-extra'); +const path = require('path'); +const g2js = require('gradle-to-js/lib/parser'); -const testProjectPath = path.join(__dirname, "../../test-app"); -const androidPath = path.join(testProjectPath, "android"); -const mainBuildGradlePath = path.join(androidPath, "build.gradle"); +const testProjectPath = testAppPath(); +const androidPath = path.join(testProjectPath, 'android'); +const mainBuildGradlePath = path.join(androidPath, 'build.gradle'); -test("Plugin injects expted dependencies in the main Gradle build file", async () => { - const content = await fs.readFile(mainBuildGradlePath, "utf8"); +test('Plugin injects expted dependencies in the main Gradle build file', async () => { + const mainBuildGradleContent = await fs.readFile(mainBuildGradlePath, "utf8"); + const gradleFileAsJson = await g2js.parseFile(mainBuildGradlePath); - expect(content).toMatchSnapshot(); + const hasBuildScriptDependency = gradleFileAsJson.buildscript.dependencies.some( + (dependency) => + dependency.group === 'com.google.gms' && + dependency.name === 'google-services' && + dependency.type === 'classpath' && + dependency.version === '4.3.13' + ); + expect(hasBuildScriptDependency).toBe(true); + expect(mainBuildGradleContent).toContain('maven { url "https://maven.gist.build" }'); }); diff --git a/__tests__/ios/apn/AppDelegate-impl.test.js b/__tests__/ios/apn/AppDelegate-impl.test.js index 4d9ab51c..e5218493 100644 --- a/__tests__/ios/apn/AppDelegate-impl.test.js +++ b/__tests__/ios/apn/AppDelegate-impl.test.js @@ -1,12 +1,126 @@ -const fs = require("fs-extra"); -const path = require("path"); +const { testAppPath, testAppName } = require('../../utils'); +const fs = require('fs-extra'); +const path = require('path'); -const testProjectPath = path.join(__dirname, "../../../test-app"); -const iosPath = path.join(testProjectPath, "ios"); -const appDelegateImplPath = path.join(iosPath, "ExpoTestbed/AppDelegate.mm"); +const testProjectPath = testAppPath(); +const iosPath = path.join(testProjectPath, 'ios'); +const appDelegateImplPath = path.join( + iosPath, + `${testAppName()}/AppDelegate.mm` +); -test("Plugin injects CIO imports and calls into AppDelegate.mm", async () => { - const content = await fs.readFile(appDelegateImplPath, "utf8"); +test('Plugin injects CIO imports and calls into AppDelegate.mm', async () => { + const content = await fs.readFile(appDelegateImplPath, 'utf8'); - expect(content).toMatchSnapshot(); + expect(content).toMatchInlineSnapshot(` + " + #if __has_include() + #import + #endif + + + // Add swift bridge imports + #import + #import <${testAppName()}-Swift.h> + + #import "AppDelegate.h" + + #import + #import + + @implementation AppDelegate + + + CIOAppPushNotificationsHandler* pnHandlerObj = [[CIOAppPushNotificationsHandler alloc] init]; + + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions + { + self.moduleName = @"main"; + + // You can add your custom initial props in the dictionary below. + // They will be passed down to the ViewController used by React Native. + self.initialProps = @{}; + + + [pnHandlerObj initializeCioSdk]; + + // Code to make the CIO SDK compatible with expo-notifications package. + // + // The CIO SDK and expo-notifications both need to handle when a push gets clicked. However, iOS only allows one click handler to be set per app. + // To get around this limitation, we set the CIO SDK as the click handler. The CIO SDK sets itself up so that when another SDK or host iOS app + // sets itself as the click handler, the CIO SDK will still be able to handle when the push gets clicked, even though it's not the designated + // click handler in iOS at runtime. + // + // This should work for most SDKs. However, expo-notifications is unique in it's implementation. It will not setup push click handling it if detects + // that another SDK or host iOS app has already set itself as the click handler: + // https://github.com/expo/expo/blob/1b29637bec0b9888e8bc8c310476293a3e2d9786/packages/expo-notifications/ios/EXNotifications/Notifications/EXNotificationCenterDelegate.m#L31-L37 + // ...to get around this, we must manually set it as the click handler after the CIO SDK. That's what this code block does. + // + // Note: Initialize the native iOS SDK and setup SDK push click handling before running this code. + # if __has_include() + // Creating a new instance, as the comments in expo-notifications suggests, does not work. With this code, if you send a CIO push to device and click on it, + // no push metrics reporting will occur. + // EXNotificationCenterDelegate *notificationCenterDelegate = [[EXNotificationCenterDelegate alloc] init]; + + // ...instead, get the singleton reference from Expo. + id notificationCenterDelegate = (id) [EXModuleRegistryProvider getSingletonModuleForClass:[EXNotificationCenterDelegate class]]; + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + center.delegate = notificationCenterDelegate; + # endif + + return [super application:application didFinishLaunchingWithOptions:launchOptions]; + } + + - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge + { + return [self bundleURL]; + } + + - (NSURL *)bundleURL + { + #if DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"]; + #else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + #endif + } + + // Linking API + - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { + return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; + } + + // Universal Links + - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { + BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; + return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; + } + + // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries + - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken + { + + [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; + return [pnHandlerObj application:application deviceToken:deviceToken]; + + } + + // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries + - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error + { + + [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; + [pnHandlerObj application:application error:error]; + + } + + // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries + - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler + { + return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; + } + + @end + " + `); }); diff --git a/__tests__/ios/apn/NotificationService-swift.test.js b/__tests__/ios/apn/NotificationService-swift.test.js index 47e29cb1..72831c56 100644 --- a/__tests__/ios/apn/NotificationService-swift.test.js +++ b/__tests__/ios/apn/NotificationService-swift.test.js @@ -1,7 +1,8 @@ +const { testAppPath } = require("../../utils"); const fs = require("fs-extra"); const path = require("path"); -const testProjectPath = path.join(__dirname, "../../../test-app"); +const testProjectPath = testAppPath(); const iosPath = path.join(testProjectPath, "ios"); const notificationServicePath = path.join(iosPath, "NotificationService/NotificationService.swift"); diff --git a/__tests__/ios/apn/PodFile.test.js b/__tests__/ios/apn/PodFile.test.js index b4e1329e..40705447 100644 --- a/__tests__/ios/apn/PodFile.test.js +++ b/__tests__/ios/apn/PodFile.test.js @@ -1,12 +1,32 @@ +const { testAppPath } = require("../../utils"); const fs = require("fs-extra"); const path = require("path"); -const testProjectPath = path.join(__dirname, "../../../test-app"); +const testProjectPath = testAppPath(); const iosPath = path.join(testProjectPath, "ios"); const podFilePath = path.join(iosPath, "Podfile"); test("Plugin injects expected customerio-reactnative/apn and customerio-reactnative-richpush/apn in Podfile", async () => { - const content = await fs.readFile(podFilePath, "utf8"); + const content = await fs.readFile(podFilePath, "utf8"); - expect(content).toMatchSnapshot(); + // Ensure APN pod is added + expect(content).toContain("pod 'customerio-reactnative/apn', :path => '../node_modules/customerio-reactnative'"); + + // Ensure NotificationService target is added with rich push pod + const podFileAsLines = content.split('\n').map(line => line.trim()); + const startIndex = podFileAsLines.indexOf("# --- CustomerIO Notification START ---"); + const endIndex = podFileAsLines.indexOf("# --- CustomerIO Notification END ---", startIndex); + expect(startIndex).toBeGreaterThan(-1); + expect(endIndex).toBeGreaterThan(startIndex); + const targetBlock = podFileAsLines.slice(startIndex, endIndex + 1).filter(line => line.length > 0); + const expectedLines = [ + "# --- CustomerIO Notification START ---", + "target 'NotificationService' do", + "use_frameworks! :linkage => :static", + "pod 'customerio-reactnative-richpush/apn', :path => '../node_modules/customerio-reactnative'", + "end", + "# --- CustomerIO Notification END ---" + ]; + + expect(targetBlock).toEqual(expectedLines); }); diff --git a/__tests__/ios/apn/PushService-swift.test.js b/__tests__/ios/apn/PushService-swift.test.js index 0766d46f..e82dc2c4 100644 --- a/__tests__/ios/apn/PushService-swift.test.js +++ b/__tests__/ios/apn/PushService-swift.test.js @@ -1,9 +1,10 @@ +const { testAppPath, testAppName } = require("../../utils"); const fs = require("fs-extra"); const path = require("path"); -const testProjectPath = path.join(__dirname, "../../../test-app"); +const testProjectPath = testAppPath(); const iosPath = path.join(testProjectPath, "ios"); -const pushServicePath = path.join(iosPath, "ExpoTestbed/PushService.swift"); +const pushServicePath = path.join(iosPath, `${testAppName()}/PushService.swift`); test("Plugin creates expected PushService.swift", async () => { const content = await fs.readFile(pushServicePath, "utf8"); diff --git a/__tests__/ios/apn/__snapshots__/AppDelegate-impl.test.js.snap b/__tests__/ios/apn/__snapshots__/AppDelegate-impl.test.js.snap deleted file mode 100644 index b457ade7..00000000 --- a/__tests__/ios/apn/__snapshots__/AppDelegate-impl.test.js.snap +++ /dev/null @@ -1,113 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Plugin injects CIO imports and calls into AppDelegate.mm 1`] = ` -" -#if __has_include() -#import -#endif - - -// Add swift bridge imports -#import -#import - -#import "AppDelegate.h" - -#import -#import - -@implementation AppDelegate - - -CIOAppPushNotificationsHandler* pnHandlerObj = [[CIOAppPushNotificationsHandler alloc] init]; - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions -{ - self.moduleName = @"main"; - - // You can add your custom initial props in the dictionary below. - // They will be passed down to the ViewController used by React Native. - self.initialProps = @{}; - - - [pnHandlerObj initializeCioSdk]; - -// Code to make the CIO SDK compatible with expo-notifications package. -// -// The CIO SDK and expo-notifications both need to handle when a push gets clicked. However, iOS only allows one click handler to be set per app. -// To get around this limitation, we set the CIO SDK as the click handler. The CIO SDK sets itself up so that when another SDK or host iOS app -// sets itself as the click handler, the CIO SDK will still be able to handle when the push gets clicked, even though it's not the designated -// click handler in iOS at runtime. -// -// This should work for most SDKs. However, expo-notifications is unique in it's implementation. It will not setup push click handling it if detects -// that another SDK or host iOS app has already set itself as the click handler: -// https://github.com/expo/expo/blob/1b29637bec0b9888e8bc8c310476293a3e2d9786/packages/expo-notifications/ios/EXNotifications/Notifications/EXNotificationCenterDelegate.m#L31-L37 -// ...to get around this, we must manually set it as the click handler after the CIO SDK. That's what this code block does. -// -// Note: Initialize the native iOS SDK and setup SDK push click handling before running this code. -# if __has_include() - // Creating a new instance, as the comments in expo-notifications suggests, does not work. With this code, if you send a CIO push to device and click on it, - // no push metrics reporting will occur. - // EXNotificationCenterDelegate *notificationCenterDelegate = [[EXNotificationCenterDelegate alloc] init]; - - // ...instead, get the singleton reference from Expo. - id notificationCenterDelegate = (id) [EXModuleRegistryProvider getSingletonModuleForClass:[EXNotificationCenterDelegate class]]; - UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; - center.delegate = notificationCenterDelegate; -# endif - - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge -{ - return [self bundleURL]; -} - -- (NSURL *)bundleURL -{ -#if DEBUG - return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"]; -#else - return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; -#endif -} - -// Linking API -- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { - return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; -} - -// Universal Links -- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { - BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; - return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; -} - -// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries -- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken -{ - - [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; - return [pnHandlerObj application:application deviceToken:deviceToken]; - -} - -// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries -- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error -{ - - [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; - [pnHandlerObj application:application error:error]; - -} - -// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries -- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler -{ - return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; -} - -@end -" -`; diff --git a/__tests__/ios/apn/__snapshots__/PodFile.test.js.snap b/__tests__/ios/apn/__snapshots__/PodFile.test.js.snap deleted file mode 100644 index ce088462..00000000 --- a/__tests__/ios/apn/__snapshots__/PodFile.test.js.snap +++ /dev/null @@ -1,79 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Plugin injects expected customerio-reactnative/apn and customerio-reactnative-richpush/apn in Podfile 1`] = ` -"require File.join(File.dirname(\`node --print "require.resolve('expo/package.json')"\`), "scripts/autolinking") -require File.join(File.dirname(\`node --print "require.resolve('react-native/package.json')"\`), "scripts/react_native_pods") - -require 'json' -podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} - -ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0' -ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR'] - -platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1' -install! 'cocoapods', - :deterministic_uuids => false - -prepare_react_native_project! - -target 'ExpoTestbed' do - use_expo_modules! - - if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' - config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; - else - config_command = [ - 'node', - '--no-warnings', - '--eval', - 'require(require.resolve(\\'expo-modules-autolinking\\', { paths: [require.resolve(\\'expo/package.json\\')] }))(process.argv.slice(1))', - 'react-native-config', - '--json', - '--platform', - 'ios' - ] - end - - config = use_native_modules!(config_command) - - use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] - use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] - - use_react_native!( - :path => config[:reactNativePath], - :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', - # An absolute path to your application root. - :app_path => "#{Pod::Config.instance.installation_root}/..", - :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false', - ) - -# --- CustomerIO Host App START --- - pod 'customerio-reactnative/apn', :path => '../node_modules/customerio-reactnative' -# --- CustomerIO Host App END --- - post_install do |installer| - react_native_post_install( - installer, - config[:reactNativePath], - :mac_catalyst_enabled => false, - :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true', - ) - - # This is necessary for Xcode 14, because it signs resource bundles by default - # when building for devices. - installer.target_installation_results.pod_target_installation_results - .each do |pod_name, target_installation_result| - target_installation_result.resource_bundle_targets.each do |resource_bundle_target| - resource_bundle_target.build_configurations.each do |config| - config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' - end - end - end - end -end -# --- CustomerIO Notification START --- -target 'NotificationService' do - use_frameworks! :linkage => :static - pod 'customerio-reactnative-richpush/apn', :path => '../node_modules/customerio-reactnative' -end -# --- CustomerIO Notification END ---" -`; diff --git a/__tests__/ios/common/AppDelegate-header.test.js b/__tests__/ios/common/AppDelegate-header.test.js index 71763fea..da3da411 100644 --- a/__tests__/ios/common/AppDelegate-header.test.js +++ b/__tests__/ios/common/AppDelegate-header.test.js @@ -1,9 +1,10 @@ +const { testAppPath, testAppName } = require("../../utils"); const fs = require("fs-extra"); const path = require("path"); -const testProjectPath = path.join(__dirname, "../../../test-app"); +const testProjectPath = testAppPath(); const iosPath = path.join(testProjectPath, "ios"); -const appDelegateHeaderPath = path.join(iosPath, "ExpoTestbed/AppDelegate.h"); +const appDelegateHeaderPath = path.join(iosPath, `${testAppName()}/AppDelegate.h`); test("Plugin injects CIO imports and calls into AppDelegate.h", async () => { const content = await fs.readFile(appDelegateHeaderPath, "utf8"); diff --git a/__tests__/ios/common/NotificationService-header.test.js b/__tests__/ios/common/NotificationService-header.test.js index 17af1850..88582766 100644 --- a/__tests__/ios/common/NotificationService-header.test.js +++ b/__tests__/ios/common/NotificationService-header.test.js @@ -1,7 +1,8 @@ +const { testAppPath } = require("../../utils"); const fs = require("fs-extra"); const path = require("path"); -const testProjectPath = path.join(__dirname, "../../../test-app"); +const testProjectPath = testAppPath(); const iosPath = path.join(testProjectPath, "ios"); const notificationServiceHeaderPath = path.join(iosPath, "NotificationService/NotificationService.h"); diff --git a/__tests__/ios/common/NotificationService-impl.test.js b/__tests__/ios/common/NotificationService-impl.test.js index a9d0a7ce..86cf38ee 100644 --- a/__tests__/ios/common/NotificationService-impl.test.js +++ b/__tests__/ios/common/NotificationService-impl.test.js @@ -1,7 +1,8 @@ +const { testAppPath } = require("../../utils"); const fs = require("fs-extra"); const path = require("path"); -const testProjectPath = path.join(__dirname, "../../../test-app"); +const testProjectPath = testAppPath(); const iosPath = path.join(testProjectPath, "ios"); const notificationServiceImplPath = path.join(iosPath, "NotificationService/NotificationService.m"); diff --git a/__tests__/ios/common/NotificationService-info-plist.test.js b/__tests__/ios/common/NotificationService-info-plist.test.js index 4ca5e944..c7efadfe 100644 --- a/__tests__/ios/common/NotificationService-info-plist.test.js +++ b/__tests__/ios/common/NotificationService-info-plist.test.js @@ -1,7 +1,8 @@ +const { testAppPath } = require("../../utils"); const fs = require("fs-extra"); const path = require("path"); -const testProjectPath = path.join(__dirname, "../../../test-app"); +const testProjectPath = testAppPath(); const iosPath = path.join(testProjectPath, "ios"); const notificationServiceInfoPlistPath = path.join(iosPath, "NotificationService/NotificationService-Info.plist"); diff --git a/__tests__/ios/fcm/AppDelegate-impl.test.js b/__tests__/ios/fcm/AppDelegate-impl.test.js index 4d9ab51c..aa062fb7 100644 --- a/__tests__/ios/fcm/AppDelegate-impl.test.js +++ b/__tests__/ios/fcm/AppDelegate-impl.test.js @@ -1,12 +1,129 @@ -const fs = require("fs-extra"); -const path = require("path"); +const { testAppPath, testAppName } = require('../../utils'); +const fs = require('fs-extra'); +const path = require('path'); -const testProjectPath = path.join(__dirname, "../../../test-app"); -const iosPath = path.join(testProjectPath, "ios"); -const appDelegateImplPath = path.join(iosPath, "ExpoTestbed/AppDelegate.mm"); +const testProjectPath = testAppPath(); +const iosPath = path.join(testProjectPath, 'ios'); +const appDelegateImplPath = path.join( + iosPath, + `${testAppName()}/AppDelegate.mm` +); -test("Plugin injects CIO imports and calls into AppDelegate.mm", async () => { - const content = await fs.readFile(appDelegateImplPath, "utf8"); +test('Plugin injects CIO imports and calls into AppDelegate.mm', async () => { + const content = await fs.readFile(appDelegateImplPath, 'utf8'); - expect(content).toMatchSnapshot(); + // expect(content).toMatchSnapshot(); + + expect(content).toMatchInlineSnapshot(` + " + #if __has_include() + #import + #endif + + @protocol FIRMessagingDelegate; + + // Add swift bridge imports + #import + #import <${testAppName()}-Swift.h> + + #import "AppDelegate.h" + + #import + #import + + @implementation AppDelegate + + + CIOAppPushNotificationsHandler* pnHandlerObj = [[CIOAppPushNotificationsHandler alloc] init]; + + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions + { + self.moduleName = @"main"; + + // You can add your custom initial props in the dictionary below. + // They will be passed down to the ViewController used by React Native. + self.initialProps = @{}; + + + [pnHandlerObj initializeCioSdk]; + + // Code to make the CIO SDK compatible with expo-notifications package. + // + // The CIO SDK and expo-notifications both need to handle when a push gets clicked. However, iOS only allows one click handler to be set per app. + // To get around this limitation, we set the CIO SDK as the click handler. The CIO SDK sets itself up so that when another SDK or host iOS app + // sets itself as the click handler, the CIO SDK will still be able to handle when the push gets clicked, even though it's not the designated + // click handler in iOS at runtime. + // + // This should work for most SDKs. However, expo-notifications is unique in it's implementation. It will not setup push click handling it if detects + // that another SDK or host iOS app has already set itself as the click handler: + // https://github.com/expo/expo/blob/1b29637bec0b9888e8bc8c310476293a3e2d9786/packages/expo-notifications/ios/EXNotifications/Notifications/EXNotificationCenterDelegate.m#L31-L37 + // ...to get around this, we must manually set it as the click handler after the CIO SDK. That's what this code block does. + // + // Note: Initialize the native iOS SDK and setup SDK push click handling before running this code. + # if __has_include() + // Creating a new instance, as the comments in expo-notifications suggests, does not work. With this code, if you send a CIO push to device and click on it, + // no push metrics reporting will occur. + // EXNotificationCenterDelegate *notificationCenterDelegate = [[EXNotificationCenterDelegate alloc] init]; + + // ...instead, get the singleton reference from Expo. + id notificationCenterDelegate = (id) [EXModuleRegistryProvider getSingletonModuleForClass:[EXNotificationCenterDelegate class]]; + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + center.delegate = notificationCenterDelegate; + # endif + + return [super application:application didFinishLaunchingWithOptions:launchOptions]; + } + + - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge + { + return [self bundleURL]; + } + + - (NSURL *)bundleURL + { + #if DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"]; + #else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + #endif + } + + // Linking API + - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { + return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; + } + + // Universal Links + - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { + BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; + return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; + } + + // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries + - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken + { + + [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; + return [pnHandlerObj application:application deviceToken:deviceToken]; + + } + + // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries + - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error + { + + [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; + [pnHandlerObj application:application error:error]; + + } + + // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries + - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler + { + return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; + } + + @end + " + `); }); diff --git a/__tests__/ios/fcm/GoogleService-InfoCopied.test.js b/__tests__/ios/fcm/GoogleService-InfoCopied.test.js index 748b5980..571d6ad0 100644 --- a/__tests__/ios/fcm/GoogleService-InfoCopied.test.js +++ b/__tests__/ios/fcm/GoogleService-InfoCopied.test.js @@ -1,7 +1,8 @@ +const { testAppPath } = require("../../utils"); const fs = require("fs-extra"); const path = require("path"); -const testProjectPath = path.join(__dirname, "../../../test-app"); +const testProjectPath = testAppPath(); const iosPath = path.join(testProjectPath, "ios"); const googleServicesFile = path.join(iosPath, "GoogleService-Info.plist"); diff --git a/__tests__/ios/fcm/NotificationService-swift.test.js b/__tests__/ios/fcm/NotificationService-swift.test.js index 47e29cb1..72831c56 100644 --- a/__tests__/ios/fcm/NotificationService-swift.test.js +++ b/__tests__/ios/fcm/NotificationService-swift.test.js @@ -1,7 +1,8 @@ +const { testAppPath } = require("../../utils"); const fs = require("fs-extra"); const path = require("path"); -const testProjectPath = path.join(__dirname, "../../../test-app"); +const testProjectPath = testAppPath(); const iosPath = path.join(testProjectPath, "ios"); const notificationServicePath = path.join(iosPath, "NotificationService/NotificationService.swift"); diff --git a/__tests__/ios/fcm/PodFile.test.js b/__tests__/ios/fcm/PodFile.test.js index b4e1329e..59963b40 100644 --- a/__tests__/ios/fcm/PodFile.test.js +++ b/__tests__/ios/fcm/PodFile.test.js @@ -1,12 +1,32 @@ +const { testAppPath } = require("../../utils"); const fs = require("fs-extra"); const path = require("path"); -const testProjectPath = path.join(__dirname, "../../../test-app"); +const testProjectPath = testAppPath(); const iosPath = path.join(testProjectPath, "ios"); const podFilePath = path.join(iosPath, "Podfile"); -test("Plugin injects expected customerio-reactnative/apn and customerio-reactnative-richpush/apn in Podfile", async () => { - const content = await fs.readFile(podFilePath, "utf8"); +test("Plugin injects expected customerio-reactnative/fcm and customerio-reactnative-richpush/fcm in Podfile", async () => { + const content = await fs.readFile(podFilePath, "utf8"); - expect(content).toMatchSnapshot(); + // Ensure FCM pod is added + expect(content).toContain("pod 'customerio-reactnative/fcm', :path => '../node_modules/customerio-reactnative'"); + + // Ensure NotificationService target is added with rich push pod + const podFileAsLines = content.split('\n').map(line => line.trim()); + const startIndex = podFileAsLines.indexOf("# --- CustomerIO Notification START ---"); + const endIndex = podFileAsLines.indexOf("# --- CustomerIO Notification END ---", startIndex); + expect(startIndex).toBeGreaterThan(-1); + expect(endIndex).toBeGreaterThan(startIndex); + const targetBlock = podFileAsLines.slice(startIndex, endIndex + 1).filter(line => line.length > 0); + const expectedLines = [ + "# --- CustomerIO Notification START ---", + "target 'NotificationService' do", + "use_frameworks! :linkage => :static", + "pod 'customerio-reactnative-richpush/fcm', :path => '../node_modules/customerio-reactnative'", + "end", + "# --- CustomerIO Notification END ---" + ]; + + expect(targetBlock).toEqual(expectedLines); }); diff --git a/__tests__/ios/fcm/PushService-swift.test.js b/__tests__/ios/fcm/PushService-swift.test.js index 0766d46f..e82dc2c4 100644 --- a/__tests__/ios/fcm/PushService-swift.test.js +++ b/__tests__/ios/fcm/PushService-swift.test.js @@ -1,9 +1,10 @@ +const { testAppPath, testAppName } = require("../../utils"); const fs = require("fs-extra"); const path = require("path"); -const testProjectPath = path.join(__dirname, "../../../test-app"); +const testProjectPath = testAppPath(); const iosPath = path.join(testProjectPath, "ios"); -const pushServicePath = path.join(iosPath, "ExpoTestbed/PushService.swift"); +const pushServicePath = path.join(iosPath, `${testAppName()}/PushService.swift`); test("Plugin creates expected PushService.swift", async () => { const content = await fs.readFile(pushServicePath, "utf8"); diff --git a/__tests__/ios/fcm/__snapshots__/AppDelegate-impl.test.js.snap b/__tests__/ios/fcm/__snapshots__/AppDelegate-impl.test.js.snap deleted file mode 100644 index ffff9eea..00000000 --- a/__tests__/ios/fcm/__snapshots__/AppDelegate-impl.test.js.snap +++ /dev/null @@ -1,114 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Plugin injects CIO imports and calls into AppDelegate.mm 1`] = ` -" -#if __has_include() -#import -#endif - -@protocol FIRMessagingDelegate; - -// Add swift bridge imports -#import -#import - -#import "AppDelegate.h" - -#import -#import - -@implementation AppDelegate - - -CIOAppPushNotificationsHandler* pnHandlerObj = [[CIOAppPushNotificationsHandler alloc] init]; - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions -{ - self.moduleName = @"main"; - - // You can add your custom initial props in the dictionary below. - // They will be passed down to the ViewController used by React Native. - self.initialProps = @{}; - - - [pnHandlerObj initializeCioSdk]; - -// Code to make the CIO SDK compatible with expo-notifications package. -// -// The CIO SDK and expo-notifications both need to handle when a push gets clicked. However, iOS only allows one click handler to be set per app. -// To get around this limitation, we set the CIO SDK as the click handler. The CIO SDK sets itself up so that when another SDK or host iOS app -// sets itself as the click handler, the CIO SDK will still be able to handle when the push gets clicked, even though it's not the designated -// click handler in iOS at runtime. -// -// This should work for most SDKs. However, expo-notifications is unique in it's implementation. It will not setup push click handling it if detects -// that another SDK or host iOS app has already set itself as the click handler: -// https://github.com/expo/expo/blob/1b29637bec0b9888e8bc8c310476293a3e2d9786/packages/expo-notifications/ios/EXNotifications/Notifications/EXNotificationCenterDelegate.m#L31-L37 -// ...to get around this, we must manually set it as the click handler after the CIO SDK. That's what this code block does. -// -// Note: Initialize the native iOS SDK and setup SDK push click handling before running this code. -# if __has_include() - // Creating a new instance, as the comments in expo-notifications suggests, does not work. With this code, if you send a CIO push to device and click on it, - // no push metrics reporting will occur. - // EXNotificationCenterDelegate *notificationCenterDelegate = [[EXNotificationCenterDelegate alloc] init]; - - // ...instead, get the singleton reference from Expo. - id notificationCenterDelegate = (id) [EXModuleRegistryProvider getSingletonModuleForClass:[EXNotificationCenterDelegate class]]; - UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; - center.delegate = notificationCenterDelegate; -# endif - - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge -{ - return [self bundleURL]; -} - -- (NSURL *)bundleURL -{ -#if DEBUG - return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"]; -#else - return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; -#endif -} - -// Linking API -- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { - return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; -} - -// Universal Links -- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { - BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; - return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; -} - -// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries -- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken -{ - - [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; - return [pnHandlerObj application:application deviceToken:deviceToken]; - -} - -// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries -- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error -{ - - [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; - [pnHandlerObj application:application error:error]; - -} - -// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries -- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler -{ - return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; -} - -@end -" -`; diff --git a/__tests__/ios/fcm/__snapshots__/PodFile.test.js.snap b/__tests__/ios/fcm/__snapshots__/PodFile.test.js.snap deleted file mode 100644 index 773ca373..00000000 --- a/__tests__/ios/fcm/__snapshots__/PodFile.test.js.snap +++ /dev/null @@ -1,79 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Plugin injects expected customerio-reactnative/apn and customerio-reactnative-richpush/apn in Podfile 1`] = ` -"require File.join(File.dirname(\`node --print "require.resolve('expo/package.json')"\`), "scripts/autolinking") -require File.join(File.dirname(\`node --print "require.resolve('react-native/package.json')"\`), "scripts/react_native_pods") - -require 'json' -podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} - -ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0' -ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR'] - -platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1' -install! 'cocoapods', - :deterministic_uuids => false - -prepare_react_native_project! - -target 'ExpoTestbed' do - use_expo_modules! - - if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' - config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; - else - config_command = [ - 'node', - '--no-warnings', - '--eval', - 'require(require.resolve(\\'expo-modules-autolinking\\', { paths: [require.resolve(\\'expo/package.json\\')] }))(process.argv.slice(1))', - 'react-native-config', - '--json', - '--platform', - 'ios' - ] - end - - config = use_native_modules!(config_command) - - use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] - use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] - - use_react_native!( - :path => config[:reactNativePath], - :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', - # An absolute path to your application root. - :app_path => "#{Pod::Config.instance.installation_root}/..", - :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false', - ) - -# --- CustomerIO Host App START --- - pod 'customerio-reactnative/fcm', :path => '../node_modules/customerio-reactnative' -# --- CustomerIO Host App END --- - post_install do |installer| - react_native_post_install( - installer, - config[:reactNativePath], - :mac_catalyst_enabled => false, - :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true', - ) - - # This is necessary for Xcode 14, because it signs resource bundles by default - # when building for devices. - installer.target_installation_results.pod_target_installation_results - .each do |pod_name, target_installation_result| - target_installation_result.resource_bundle_targets.each do |resource_bundle_target| - resource_bundle_target.build_configurations.each do |config| - config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' - end - end - end - end -end -# --- CustomerIO Notification START --- -target 'NotificationService' do - use_frameworks! :linkage => :static - pod 'customerio-reactnative-richpush/fcm', :path => '../node_modules/customerio-reactnative' -end -# --- CustomerIO Notification END ---" -`; diff --git a/__tests__/utils.js b/__tests__/utils.js new file mode 100644 index 00000000..b56fa7f3 --- /dev/null +++ b/__tests__/utils.js @@ -0,0 +1,15 @@ +const path = require("path"); + +function testAppPath() { + const appPath = process.env.TEST_APP_PATH + if (appPath) { + return path.join(appPath) + } + return path.join(__dirname, "../test-app") +} + +function testAppName() { + return process.env.TEST_APP_NAME || "ExpoTestbed" +} + +module.exports = { testAppPath, testAppName }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 14f467e9..24b677ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "eslint-plugin-prettier": "^4.0.0", "expo-build-properties": "^0.2.0", "expo-module-scripts": "^2.0.0", + "gradle-to-js": "^2.0.1", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "prettier": "^2.6.2", @@ -2661,9 +2662,9 @@ } }, "node_modules/@expo/cli/node_modules/@expo/config-plugins": { - "version": "9.0.16", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.16.tgz", - "integrity": "sha512-AnJzmFB7ztM0JZBn+Ut6BQYC2WeGDzfIhBZVOIPMQbdBqvwJ7TmFEsGTGSxdwU/VqJaJK2sWxyt1zbWkpIYCEA==", + "version": "9.0.17", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.17.tgz", + "integrity": "sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg==", "dev": true, "license": "MIT", "peer": true, @@ -2920,9 +2921,9 @@ "license": "MIT" }, "node_modules/@expo/config/node_modules/@expo/config-plugins": { - "version": "9.0.16", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.16.tgz", - "integrity": "sha512-AnJzmFB7ztM0JZBn+Ut6BQYC2WeGDzfIhBZVOIPMQbdBqvwJ7TmFEsGTGSxdwU/VqJaJK2sWxyt1zbWkpIYCEA==", + "version": "9.0.17", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.17.tgz", + "integrity": "sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg==", "dev": true, "license": "MIT", "peer": true, @@ -3526,9 +3527,9 @@ } }, "node_modules/@expo/prebuild-config/node_modules/@expo/config-plugins": { - "version": "9.0.16", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.16.tgz", - "integrity": "sha512-AnJzmFB7ztM0JZBn+Ut6BQYC2WeGDzfIhBZVOIPMQbdBqvwJ7TmFEsGTGSxdwU/VqJaJK2sWxyt1zbWkpIYCEA==", + "version": "9.0.17", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.17.tgz", + "integrity": "sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg==", "dev": true, "license": "MIT", "peer": true, @@ -11992,9 +11993,9 @@ } }, "node_modules/expo/node_modules/@expo/config-plugins": { - "version": "9.0.16", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.16.tgz", - "integrity": "sha512-AnJzmFB7ztM0JZBn+Ut6BQYC2WeGDzfIhBZVOIPMQbdBqvwJ7TmFEsGTGSxdwU/VqJaJK2sWxyt1zbWkpIYCEA==", + "version": "9.0.17", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.17.tgz", + "integrity": "sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg==", "dev": true, "license": "MIT", "peer": true, @@ -13127,6 +13128,19 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/gradle-to-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/gradle-to-js/-/gradle-to-js-2.0.1.tgz", + "integrity": "sha512-is3hDn9zb8XXnjbEeAEIqxTpLHUiGBqjegLmXPuyMBfKAggpadWFku4/AP8iYAGBX6qR9/5UIUIp47V0XI3aMw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.merge": "^4.6.2" + }, + "bin": { + "gradle-to-js": "cli.js" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", diff --git a/package.json b/package.json index cf576021..796e2aa6 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,8 @@ "@types/jest": "^29.5.14", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "ts-jest": "^29.2.5" + "ts-jest": "^29.2.5", + "gradle-to-js": "^2.0.1" }, "commitlint": { "extends": [ diff --git a/scripts/compatibility/validate-plugin.js b/scripts/compatibility/validate-plugin.js index 3763c38b..89703a62 100644 --- a/scripts/compatibility/validate-plugin.js +++ b/scripts/compatibility/validate-plugin.js @@ -26,6 +26,15 @@ if (CLEAN_FLAG) PREBUILD_CMD += " --clean"; * @returns {string} - The name of the workspace. */ function getIosWorkspaceName(fallback = "App") { + // Scan the /ios directory for .xcworkspace files + const iosPath = path.join(APP_PATH, "ios"); + if (fs.existsSync(iosPath)) { + const workspaces = fs.readdirSync(iosPath).filter((file) => file.endsWith(".xcworkspace")); + if (workspaces.length > 0) { + return path.basename(workspaces[0], ".xcworkspace"); + } + } + // Try to get workspace name from app.json const appJsonPath = path.join(APP_PATH, "app.json"); if (fs.existsSync(appJsonPath)) { @@ -39,15 +48,6 @@ function getIosWorkspaceName(fallback = "App") { } } - // Scan the /ios directory for .xcworkspace files - const iosPath = path.join(APP_PATH, "ios"); - if (fs.existsSync(iosPath)) { - const workspaces = fs.readdirSync(iosPath).filter((file) => file.endsWith(".xcworkspace")); - if (workspaces.length > 0) { - return path.basename(workspaces[0], ".xcworkspace"); - } - } - // Default fallback return fallback; } @@ -63,7 +63,7 @@ function execute() { runCommand(PREBUILD_CMD); logMessage("🧪 Running Android tests..."); - runCommand(`npm test -- ${TESTS_DIRECTORY_PATH}/android`); + runCommand(`TEST_APP_PATH=${APP_PATH} npm test -- ${TESTS_DIRECTORY_PATH}/android`); logMessage("🤖 Building Android project..."); try { @@ -85,8 +85,9 @@ function execute() { ); runCommand(PREBUILD_CMD); + const JEST_TEST_ENV_VALUES = `TEST_APP_PATH=${APP_PATH} TEST_APP_NAME=${getIosWorkspaceName()}`; logMessage(`🧪 Running iOS tests for provider: ${provider}`); - runCommand(`npm test -- ${TESTS_DIRECTORY_PATH}/ios/common ${TESTS_DIRECTORY_PATH}/ios/${provider}`); + runCommand(`${JEST_TEST_ENV_VALUES} npm test -- ${TESTS_DIRECTORY_PATH}/ios/common ${TESTS_DIRECTORY_PATH}/ios/${provider}`); logMessage(`📱 Building iOS project for provider: ${provider}`); try {