Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ npx expo run:android
| `dataPlanId` | string | No | Data plan ID for validation |
| `dataPlanVersion` | number | No | Data plan version (ignored unless `dataPlanId` is also set) |
| `iosKits` | string[] | No | iOS kit pod names (e.g., `['mParticle-Rokt']`) |
| `iosDependencyManager` | string | No | `'cocoapods'` (default) or `'spm'`: take the iOS SDK and kits from Swift Package Manager (see below) |
| `iosSdkVersion` | string | No | With `'spm'`, the exact mParticle core SDK version, also used for kits without a version |
| `iosSpmKits` | object[] | No | With `'spm'`, extra kits as `{ url, product, version? }` Swift packages |
| `customBaseUrl` | string | No | Custom base URL for global CNAME setup on iOS and Android; must be an absolute `https://` URL or prebuild fails |
| `pinningDisabled` | boolean | No | Disable SSL pinning (`MPNetworkOptions` on iOS; `setPinningDisabledInDevelopment` on Android) |
| `androidKits` | string[] | No | Android kit artifact names (e.g., `['android-rokt-kit']`) |
Expand Down Expand Up @@ -128,6 +131,21 @@ For global CNAME setup, add the optional shared `customBaseUrl` setting:
}
```

### Swift Package Manager (Expo)

Set `iosDependencyManager` to `'spm'` to take the iOS mParticle SDK and kits from Swift Package Manager instead of CocoaPods ([details](#swift-package-manager-opt-in)). `iosKits` names are mapped to their Swift packages; today that is `mParticle-Rokt`. List any other kit in `iosSpmKits`, or prebuild fails with an error naming it.

```json
{
"iosDependencyManager": "spm",
"iosKits": ["mParticle-Rokt"]
}
```

Expo regenerates `ios/` on `expo prebuild --clean`, so there is no committed `Package.resolved`. Set `iosSdkVersion` to pin the core SDK (and kits without a version) exactly. With `'spm'`, the core SDK package is always linked, so `import mParticle_Apple_SDK` resolves without declaring the umbrella pod.

When you switch `iosDependencyManager` in either direction, run `npx expo prebuild --clean`, so the previous mode's Podfile lines and Swift packages are not left behind.

### What the Plugin Does

**iOS:**
Expand All @@ -136,6 +154,7 @@ For global CNAME setup, add the optional shared `customBaseUrl` setting:
- Sets `MPNetworkOptions` (`customBaseURL` and/or `pinningDisabled`) before startup when those plugin options are configured
- Configures `pre_install` hook in Podfile for dynamic framework linking, covering the kit's transitive pods (skipped if the Podfile already mentions `mParticle-Apple-SDK`)
- Adds specified kit pod dependencies — `mParticle-Rokt` is pinned to `>= 9.3.1, < 10.0`, other kits are added unpinned
- With `iosDependencyManager: 'spm'`, instead of the two items above: turns on this package's Swift Package Manager mode in the Podfile and calls `mparticle_spm_post_install`, which links the core SDK and kits into the app target as Swift packages pinned to exact versions

**Android:**

Expand Down Expand Up @@ -379,7 +398,7 @@ Every mParticle and Rokt SDK must then come from Swift Package Manager, and none
])
```

`core_version:` pins the core SDK; it defaults to the version this release was tested with.
`core_version:` pins the core SDK; it defaults to the version this release was tested with. A kit without a `version:` gets the core's version, since mParticle kits are released with the core.

4. Run `pod install`, then build as usual.

Expand Down
4 changes: 3 additions & 1 deletion ios/mparticle_spm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ def self.add_package(project, target, url:, product:, version:)

def mparticle_spm_post_install(installer, core_version: MParticleSPM::DEFAULT_CORE_VERSION, kits: [])
MParticleSPM.guard!(installer)
packages = [{ url: MParticleSPM::CORE_URL, product: MParticleSPM::CORE_PRODUCT, version: core_version }] + kits
# mParticle kits are released in lockstep with the core, so a kit without a version gets the core's.
packages = [{ url: MParticleSPM::CORE_URL, product: MParticleSPM::CORE_PRODUCT, version: core_version }] +
kits.map { |kit| { version: core_version }.merge(kit) }
touched = {}
MParticleSPM.application_targets(installer).each do |project, target|
packages.each do |pkg|
Expand Down
163 changes: 163 additions & 0 deletions js/__tests__/plugin-ios-spm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { applyMParticlePodfileMods } from '../../plugin/src/withMParticleIOS';
import type { MParticlePluginProps } from '../../plugin/src/withMParticle';

/**
* The Expo config plugin's Podfile changes. `iosDependencyManager: 'cocoapods'` (the default)
* must keep producing exactly what earlier releases produced; `'spm'` turns on the pod's Swift
* Package Manager mode and calls ios/mparticle_spm.rb instead of adding pods.
*/

// The part of the Expo prebuild template the plugin touches.
const EXPO_PODFILE = `require File.join(File.dirname(\`node --print "require.resolve('expo/package.json')"\`), "scripts/autolinking")
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'

prepare_react_native_project!

target 'MyApp' do
use_expo_modules!
config = use_native_modules!(config_command)

use_react_native!(
:path => config[:reactNativePath],
:app_path => "#{Pod::Config.instance.installation_root}/..",
)

post_install do |installer|
react_native_post_install(
installer,
config[:reactNativePath],
)
end
end
`;

const baseProps: MParticlePluginProps = {
iosApiKey: 'key',
iosApiSecret: 'secret',
androidApiKey: 'key',
androidApiSecret: 'secret',
iosKits: ['mParticle-Rokt'],
};

const spmProps: MParticlePluginProps = {
...baseProps,
iosDependencyManager: 'spm',
};

const ROKT_CALL =
" mparticle_spm_post_install(installer, kits: [{ url: 'https://github.com/mparticle-integrations/mp-apple-integration-rokt', product: 'mParticle-Rokt' }])";

describe('applyMParticlePodfileMods', () => {
it('keeps the CocoaPods output unchanged', () => {
expect(applyMParticlePodfileMods(EXPO_PODFILE, baseProps)).toBe(
EXPO_PODFILE.replace(
"'15.1'\n",
`'15.1'

# mParticle dynamic framework linking (added by react-native-mparticle expo plugin)
pre_install do |installer|
installer.pod_targets.each do |pod|
if pod.name == 'mParticle-Apple-SDK' || pod.name == 'mParticle-Apple-SDK-ObjC' || pod.name == 'mParticle-Apple-SDK-Swift' || pod.name == 'mParticle-Rokt' || pod.name == 'Rokt-Widget' || pod.name == 'RoktContracts' || pod.name == 'RoktUXHelper' || pod.name == 'DcuiSchema'
def pod.build_type;
Pod::BuildType.new(:linkage => :dynamic, :packaging => :framework)
end
end
end
end
`
).replace(
' )\n\n post_install',
` )

# mParticle kits (added by react-native-mparticle expo plugin)
pod 'mParticle-Rokt', '>= 9.3.1', '< 10.0'

post_install`
)
);
});

it('sets the flag before the target, requires the helper and calls it in post_install', () => {
const podfile = applyMParticlePodfileMods(EXPO_PODFILE, spmProps);

expect(podfile).toBe(
EXPO_PODFILE.replace(
"target 'MyApp' do",
`# mParticle Swift Package Manager mode (added by react-native-mparticle expo plugin)
$RNMParticleUseSPM = true
require File.join(File.dirname(\`node --print "require.resolve('react-native-mparticle/package.json')"\`.strip), 'ios', 'mparticle_spm')

target 'MyApp' do`
).replace(
'post_install do |installer|\n',
`post_install do |installer|\n${ROKT_CALL}\n`
)
);
expect(podfile).not.toContain('pre_install');
expect(podfile).not.toContain("pod 'mParticle-Rokt'");
});

it('passes the core version and extra kits through', () => {
const podfile = applyMParticlePodfileMods(EXPO_PODFILE, {
...spmProps,
iosSdkVersion: '9.6.1',
iosSpmKits: [
{
url: 'https://github.com/example/kit',
product: 'Example-Kit',
version: '1.2.3',
},
],
});

expect(podfile).toContain(
"mparticle_spm_post_install(installer, core_version: '9.6.1', kits: [{ url: 'https://github.com/mparticle-integrations/mp-apple-integration-rokt', product: 'mParticle-Rokt' }, { url: 'https://github.com/example/kit', product: 'Example-Kit', version: '1.2.3' }])"
);
});

it('is idempotent, and a rerun with new settings replaces the call', () => {
const once = applyMParticlePodfileMods(EXPO_PODFILE, spmProps);
expect(applyMParticlePodfileMods(once, spmProps)).toBe(once);

const pinned = applyMParticlePodfileMods(once, {
...spmProps,
iosSdkVersion: '9.7.0',
});
expect(pinned.match(/\$RNMParticleUseSPM = true/g)).toHaveLength(1);
expect(pinned.match(/mparticle_spm_post_install\(/g)).toHaveLength(1);
expect(pinned).toContain("core_version: '9.7.0'");
});

it('rejects an iosKits name with no known Swift package', () => {
expect(() =>
applyMParticlePodfileMods(EXPO_PODFILE, {
...spmProps,
iosKits: ['mParticle-Amplitude'],
})
).toThrow(/"mParticle-Amplitude" has no known Swift package.*iosSpmKits/);
});

it('rejects values that would break out of the generated Ruby string', () => {
expect(() =>
applyMParticlePodfileMods(EXPO_PODFILE, {
...spmProps,
iosSdkVersion: "9.6.1'); system('echo",
})
).toThrow(/invalid iosSdkVersion/);
expect(() =>
applyMParticlePodfileMods(EXPO_PODFILE, {
...spmProps,
iosSpmKits: [{ url: 'http://example.com/kit', product: 'Kit' }],
})
).toThrow(/invalid Swift package URL/);
});

it('rejects an unknown dependency manager', () => {
expect(() =>
applyMParticlePodfileMods(EXPO_PODFILE, {
...baseProps,
iosDependencyManager: 'carthage' as 'spm',
})
).toThrow(/must be "cocoapods" or "spm"/);
});
});
33 changes: 33 additions & 0 deletions plugin/src/withMParticle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ import { withMParticleAndroid } from './withMParticleAndroid';

const pkg = require('../../package.json');

/**
* A kit taken from Swift Package Manager (`iosDependencyManager: 'spm'`)
*/
export interface IosSpmKit {
/** Package repository URL, e.g. `https://github.com/mparticle-integrations/mp-apple-integration-rokt` */
url: string;
/** Package product to link, e.g. `mParticle-Rokt` */
product: string;
/** Exact version; defaults to the core SDK version */
version?: string;
}

/**
* mParticle plugin configuration options
*/
Expand Down Expand Up @@ -56,6 +68,27 @@ export interface MParticlePluginProps {
*/
iosKits?: string[];

/**
* Where the iOS mParticle SDK and kits come from.
* - `'cocoapods'`: pods, as before.
* - `'spm'`: Swift packages linked into the app target (README › Swift Package Manager).
* `iosKits` names are mapped to their Swift packages; list any other kit in `iosSpmKits`.
* @default 'cocoapods'
*/
iosDependencyManager?: 'cocoapods' | 'spm';

/**
* In `'spm'` mode, the exact mParticle core SDK version, also used for kits without a version.
* @default the version this release of react-native-mparticle was tested with
*/
iosSdkVersion?: string;

/**
* In `'spm'` mode, extra kits as Swift packages.
* @example [{ url: 'https://github.com/mparticle-integrations/mp-apple-integration-rokt', product: 'mParticle-Rokt' }]
*/
iosSpmKits?: IosSpmKit[];

/**
* Custom base URL for global CNAME setup.
* This is applied before mParticle starts on iOS and Android.
Expand Down
Loading
Loading