Skip to content

Commit 529306b

Browse files
authored
feat: support for auto initializing native SDK from app.json (#292)
2 parents 7a5ce2b + 5489790 commit 529306b

65 files changed

Lines changed: 3756 additions & 665 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/lint.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Lint and TypeScript
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
lint-and-typecheck:
9+
name: Lint and TypeScript check
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-node@v4
14+
with:
15+
node-version: '20'
16+
17+
- name: Install dependencies
18+
run: npm ci
19+
20+
- name: Run lint check
21+
run: npm run lint
22+
23+
- name: Run TypeScript check
24+
run: npm run typescript

.github/workflows/test.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,18 @@ jobs:
6262
6363
- name: Run plugin tests
6464
run: npm run setup-test-app && npm test -- __tests__/ios/common __tests__/ios/${{ matrix.push_provider }}
65+
66+
test-utils:
67+
name: Test utility functions
68+
runs-on: ubuntu-latest
69+
steps:
70+
- uses: actions/checkout@v4
71+
- uses: actions/setup-node@v4
72+
with:
73+
node-version: '20'
74+
75+
- name: Install dependencies
76+
run: npm ci
77+
78+
- name: Run utils tests
79+
run: npm test -- __tests__/utils

.github/workflows/validate-plugin-compatibility.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ jobs:
101101
npm run compatibility:configure-plugin -- \
102102
--app-path=${{ env.APP_PATH }} \
103103
--add-default-config \
104+
${{ matrix.expo-version == 52 && '--use-auto-init=false' || '' }} \
104105
${{ matrix.ios-deployment-target && format('--expo-build-props.ios.deploymentTarget={0}', matrix.ios-deployment-target) }}
105106
106107
- name: Validate Plugin
File renamed without changes.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Expo 53+ MainApplication tests Plugin injects CIO initializer into MainApplication.kt 1`] = `
4+
"package io.customer.testbed.expo
5+
6+
import android.app.Application
7+
import android.content.res.Configuration
8+
9+
import com.facebook.react.PackageList
10+
import com.facebook.react.ReactApplication
11+
import com.facebook.react.ReactNativeHost
12+
import com.facebook.react.ReactPackage
13+
import com.facebook.react.ReactHost
14+
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
15+
import com.facebook.react.defaults.DefaultReactNativeHost
16+
import com.facebook.react.soloader.OpenSourceMergedSoMapping
17+
import com.facebook.soloader.SoLoader
18+
19+
import expo.modules.ApplicationLifecycleDispatcher
20+
import expo.modules.ReactNativeHostWrapper
21+
22+
import io.customer.sdk.expo.CustomerIOSDKInitializer
23+
24+
class MainApplication : Application(), ReactApplication {
25+
26+
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
27+
this,
28+
object : DefaultReactNativeHost(this) {
29+
override fun getPackages(): List<ReactPackage> {
30+
val packages = PackageList(this).packages
31+
// Packages that cannot be autolinked yet can be added manually here, for example:
32+
// packages.add(MyReactNativePackage())
33+
return packages
34+
}
35+
36+
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
37+
38+
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
39+
40+
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
41+
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
42+
}
43+
)
44+
45+
override val reactHost: ReactHost
46+
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
47+
48+
override fun onCreate() {
49+
super.onCreate()
50+
SoLoader.init(this, OpenSourceMergedSoMapping)
51+
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
52+
// If you opted-in for the New Architecture, we load the native entry point for this app.
53+
load()
54+
}
55+
ApplicationLifecycleDispatcher.onApplicationCreate(this)
56+
57+
// Auto Initialize Native Customer.io SDK
58+
CustomerIOSDKInitializer.initialize(this)
59+
}
60+
61+
override fun onConfigurationChanged(newConfig: Configuration) {
62+
super.onConfigurationChanged(newConfig)
63+
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
64+
}
65+
}
66+
"
67+
`;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const { testAppPath, getTestAppAndroidJavaSourcePath, isExpoVersion53OrHigher } = require('../utils');
2+
const fs = require('fs-extra');
3+
const path = require('path');
4+
5+
const testProjectPath = testAppPath();
6+
const androidPath = path.join(testProjectPath, 'android');
7+
8+
describe('Expo 53+ MainApplication tests', () => {
9+
const mainApplicationPath = path.join(
10+
androidPath,
11+
getTestAppAndroidJavaSourcePath(),
12+
'MainApplication.kt'
13+
);
14+
if (isExpoVersion53OrHigher()) {
15+
test('Plugin injects CIO initializer into MainApplication.kt', async () => {
16+
const content = await fs.readFile(mainApplicationPath, 'utf8');
17+
expect(content).toMatchSnapshot();
18+
});
19+
} else {
20+
test.skip('Plugin injects CIO initializer into MainApplication.kt', () => {
21+
// Skipped: Only relevant for Expo 53+
22+
});
23+
}
24+
});
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { withStringsXml } from '@expo/config-plugins';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import { addStringsToXml, withProjectStrings } from '../../plugin/src/android/withProjectStrings';
5+
import { getPluginVersion } from '../../plugin/src/utils/plugin';
6+
7+
jest.mock('@expo/config-plugins');
8+
9+
const mockWithStringsXml = withStringsXml as jest.MockedFunction<typeof withStringsXml>;
10+
11+
describe('Android Project Strings Configuration', () => {
12+
beforeEach(() => {
13+
jest.clearAllMocks();
14+
});
15+
16+
describe('Customer.io SDK client metadata strings', () => {
17+
it('should set client source to "Expo" and version to current plugin version', () => {
18+
const mockConfig = {
19+
modResults: {
20+
resources: {
21+
string: []
22+
}
23+
}
24+
};
25+
26+
mockWithStringsXml.mockImplementation((config, modifier) => {
27+
modifier(mockConfig as any);
28+
return config;
29+
});
30+
31+
// Read the actual package.json to get the expected version
32+
const packageJsonPath = path.resolve(__dirname, '../../package.json');
33+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
34+
const expectedVersion = packageJson.version;
35+
36+
withProjectStrings({} as any);
37+
38+
expect(mockConfig.modResults.resources.string).toContainEqual({
39+
$: { name: 'customer_io_react_native_sdk_client_source' },
40+
_: 'Expo'
41+
});
42+
expect(mockConfig.modResults.resources.string).toContainEqual({
43+
$: { name: 'customer_io_react_native_sdk_client_version' },
44+
_: expectedVersion
45+
});
46+
});
47+
});
48+
49+
describe('addStringsToXml utility', () => {
50+
it('should add Customer.io SDK strings to empty XML', () => {
51+
const stringsXml = {
52+
resources: {
53+
string: []
54+
}
55+
};
56+
57+
const stringResources = [
58+
{ name: 'customer_io_react_native_sdk_client_source', value: 'Expo' },
59+
{ name: 'customer_io_react_native_sdk_client_version', value: getPluginVersion() }
60+
];
61+
62+
addStringsToXml(stringsXml, stringResources);
63+
64+
expect(stringsXml.resources.string).toHaveLength(2);
65+
expect(stringsXml.resources.string[0]).toEqual({
66+
$: { name: 'customer_io_react_native_sdk_client_source' },
67+
_: 'Expo'
68+
});
69+
expect(stringsXml.resources.string[1]).toEqual({
70+
$: { name: 'customer_io_react_native_sdk_client_version' },
71+
_: getPluginVersion()
72+
});
73+
});
74+
75+
it('should update existing Customer.io SDK strings', () => {
76+
const stringsXml = {
77+
resources: {
78+
string: [
79+
{
80+
$: { name: 'customer_io_react_native_sdk_client_source' },
81+
_: 'ReactNative'
82+
},
83+
{
84+
$: { name: 'customer_io_react_native_sdk_client_version' },
85+
_: '1.0.0'
86+
}
87+
]
88+
}
89+
};
90+
91+
const stringResources = [
92+
{ name: 'customer_io_react_native_sdk_client_source', value: 'Expo' },
93+
{ name: 'customer_io_react_native_sdk_client_version', value: getPluginVersion() }
94+
];
95+
96+
addStringsToXml(stringsXml, stringResources);
97+
98+
expect(stringsXml.resources.string).toHaveLength(2);
99+
expect(stringsXml.resources.string[0]._).toBe('Expo');
100+
expect(stringsXml.resources.string[1]._).toBe(getPluginVersion());
101+
});
102+
103+
it('should create XML structure when missing', () => {
104+
const stringsXml = {};
105+
106+
const stringResources = [
107+
{ name: 'customer_io_react_native_sdk_client_source', value: 'Expo' }
108+
];
109+
110+
addStringsToXml(stringsXml as any, stringResources);
111+
112+
expect(stringsXml).toEqual({
113+
resources: {
114+
string: [
115+
{
116+
$: { name: 'customer_io_react_native_sdk_client_source' },
117+
_: 'Expo'
118+
}
119+
]
120+
}
121+
});
122+
});
123+
});
124+
});

__tests__/ios/apn/__snapshots__/CioSdkAppDelegateHandler-swift.test.js.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public class CioSdkAppDelegateHandler: NSObject {
5050
5151
_ = cioAppDelegate.application(application, didFinishLaunchingWithOptions: launchOptions)
5252
53+
// Auto Initialize Native Customer.io SDK
54+
CustomerIOSDKInitializer.initialize()
5355
MessagingPushAPN.initialize(
5456
withConfig: MessagingPushConfigBuilder()
5557
.autoFetchDeviceToken(true)

__tests__/ios/fcm/__snapshots__/CioSdkAppDelegateHandler-swift.test.js.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public class CioSdkAppDelegateHandler: NSObject {
5858
_ = cioAppDelegate.application(application, didFinishLaunchingWithOptions: launchOptions)
5959
UIApplication.shared.registerForRemoteNotifications()
6060
61+
// Auto Initialize Native Customer.io SDK
62+
CustomerIOSDKInitializer.initialize()
6163
MessagingPushFCM.initialize(
6264
withConfig: MessagingPushConfigBuilder()
6365
.autoFetchDeviceToken(true)

0 commit comments

Comments
 (0)