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
4 changes: 2 additions & 2 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ jobs:
- ios_deps: spm
check_name: iOS Sample App (SPM)
env:
# sample/ios/Podfile takes the mParticle SDKs from Swift Package Manager when this is 1.
MP_USE_SPM: ${{ matrix.ios_deps == 'spm' && '1' || '0' }}
# sample/ios/Podfile takes the mParticle SDKs from Swift Package Manager unless this is 1.
MP_USE_COCOAPODS: ${{ matrix.ios_deps == 'cocoapods' && '1' || '0' }}
steps:
- name: Checkout
uses: actions/checkout@v7
Expand Down
23 changes: 13 additions & 10 deletions MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,24 @@

This document provides migration guidance for changes in `react-native-mparticle`.

## Moving an iOS app to Swift Package Manager mode
## iOS: the mParticle SDKs now come from Swift Package Manager

Optional, iOS only. The default CocoaPods setup keeps working unchanged. See [README › Swift Package Manager (opt-in)](./README.md#swift-package-manager-opt-in) for what the mode does.
**Breaking, iOS only.** React Native and this package still install with CocoaPods, but the mParticle core SDK and its kits now come from Swift Package Manager by default, linked into the app target. CocoaPods trunk becomes read-only on 2 December 2026. An app with no kits needs no change. An app that declares kit pods fails `pod install` until it moves them, or opts out. See [README › Swift Package Manager](./README.md#swift-package-manager).

1. In `ios/Podfile`, add `$RNMParticleUseSPM = true` and `require_relative '../node_modules/react-native-mparticle/ios/mparticle_spm'` above the first `target` block.
2. Remove every mParticle and Rokt pod you declared, such as `pod 'mParticle-Rokt'`, `pod 'mParticle-Apple-SDK'` or `pod 'Rokt-Widget'`.
3. Remove the mParticle, Rokt, `RoktContracts`, `RoktUXHelper` and `DcuiSchema` names from any `pre_install` hook that makes them dynamic frameworks. If the hook lists nothing else, delete it.
4. If you added mParticle or Rokt Swift packages to the app target by hand, remove them in Xcode (target › General › Frameworks, Libraries, and Embedded Content, and the project's Package Dependencies). The helper adds them back, pinned.
5. In `post_install`, after `react_native_post_install(...)`, call `mparticle_spm_post_install(installer, kits: [...])` with one entry per kit.
6. Run `pod install`. It prints each package it adds, and fails with a list of pods to remove if any would add a second copy of the SDK.
7. Build, run a Debug build once to confirm no red box, and commit the `.xcodeproj` change and `Package.resolved`.
To move a bare React Native app:

1. In `ios/Podfile`, remove every mParticle and Rokt pod you declared, such as `pod 'mParticle-Rokt'`, `pod 'mParticle-Apple-SDK'` or `pod 'Rokt-Widget'`.
2. Remove the mParticle, Rokt, `RoktContracts`, `RoktUXHelper` and `DcuiSchema` names from any `pre_install` hook that makes them dynamic frameworks. If the hook lists nothing else, delete it.
3. If you added mParticle or Rokt Swift packages to the app target by hand, remove them in Xcode (target › General › Frameworks, Libraries, and Embedded Content, and the project's Package Dependencies). `pod install` adds them back, pinned.
4. Above the first `target` block, list your kits by CocoaPods name: `$RNMParticleSPMKits = ['mParticle-Rokt']`.
5. Run `pod install`. It prints each package it adds, and fails with a list of pods to remove if any would add a second copy of the SDK.
6. Build, run a Debug build once to confirm no red box, and commit the `.xcodeproj` change and `Package.resolved`.

A Swift `AppDelegate` needs no change: `import mParticle_Apple_SDK` resolves from the Swift package.

To go back, undo these steps: remove `$RNMParticleUseSPM` and the helper call, restore your kit pods and the `pre_install` hook, remove the packages from the app target, and run `pod install`.
Expo apps need no change beyond `npx expo prebuild --clean`: the plugin maps `iosKits` to their Swift packages. A kit the plugin does not know goes in `iosSpmKits`.

To stay on CocoaPods for now, add `$RNMParticleDisableSPM = true` at the top of `ios/Podfile`, before any `target` block, and keep your pods (Expo: `"iosDependencyManager": "cocoapods"`). This is deprecated, and a Podfile with a tvOS target that uses this package must do it.

## Migrating embedded placements to placeholder names

Expand Down
214 changes: 110 additions & 104 deletions README.md

Large diffs are not rendered by default.

184 changes: 148 additions & 36 deletions ios/mparticle_spm.rb
Original file line number Diff line number Diff line change
@@ -1,43 +1,110 @@
# Opt-in Swift Package Manager mode for the mParticle SDKs (see README › Swift Package Manager).
# Swift Package Manager mode for the mParticle SDKs, the default (see README › Swift Package Manager).
#
# Podfile usage (bare React Native):
# react-native-mparticle.podspec loads this file on every `pod install` and turns the mode on unless
# the Podfile opts out. React Native and this package still install with CocoaPods; the mParticle
# core SDK and its kits are Swift packages linked into the app target. Podfile settings, all
# optional, set before the first `target` block, which evaluates the podspec:
#
# $RNMParticleUseSPM = true # must be set before `target` / use_native_modules!
# require_relative '../node_modules/react-native-mparticle/ios/mparticle_spm'
# ...
# post_install do |installer|
# react_native_post_install(installer, ...)
# mparticle_spm_post_install(installer, kits: [
# { url: 'https://github.com/mparticle-integrations/mp-apple-integration-rokt', product: 'mParticle-Rokt', version: '9.6.1' },
# ])
# end
# $RNMParticleSPMKits = ['mParticle-Rokt'] # kits by pod name, from ios/mparticle_spm_kits.json,
# # or { url:, product:, version: } for any other kit
# $RNMParticleSPMCoreVersion = '9.6.1' # exact core SDK version; kits without one get it too
# $RNMParticleDisableSPM = true # take the SDKs from CocoaPods instead
#
# What it does:
# 1. Refuses to install when a pod would add a second copy of the mParticle / Rokt SDKs (mixing pods
# and Swift packages builds and archives silently, then crashes at runtime).
# 2. Adds the mParticle core (and any kits) as Swift packages to every iOS application target that
# uses this pod, idempotently, pinned to an exact version. The app target owns the only copy of each
# SDK.
# On each `pod install` in this mode:
# 1. Before CocoaPods validates the targets, it stops the install if a pod would add a second copy
# of the mParticle / Rokt SDKs. Mixing pods and Swift packages builds and archives silently, then
# crashes at runtime.
# 2. After the Podfile's post_install hook, it adds the core SDK and each kit as Swift packages,
# pinned to an exact version, to every iOS application target that uses this pod. The app target
# owns the only copy of each SDK.
# With the opt-out, it only warns when an app target still links the mParticle Swift packages.
require 'json'
require 'xcodeproj'

module MParticleSPM
# Keep these URLs byte-identical to the ones the mParticle kits use, so SwiftPM unifies them into one
# package identity (mparticle-apple-sdk). No `.git` suffix for the core.
CORE_URL = 'https://github.com/mParticle/mparticle-apple-sdk'.freeze
CORE_PRODUCT = 'mParticle-Apple-SDK'.freeze
# Default core version when the Podfile does not pass one. Bump with each tested native release.
# Default core version when the Podfile sets none. Bump with each tested native release.
DEFAULT_CORE_VERSION = '9.6.1'.freeze
CONFLICTING_PODS = /\A(mParticle-.*|Rokt-Widget|RoktContracts|RoktUXHelper|DcuiSchema)\z/.freeze
# Kits by their CocoaPods name. The Expo config plugin reads the same file.
KITS = JSON.parse(File.read(File.join(__dir__, 'mparticle_spm_kits.json')))['kits'].freeze
CONFLICTING_PODS =
/\A(mParticle-.*|Rokt-Widget|RoktContracts|RoktUXHelper|DcuiSchema|RoktSDKPlus|RoktPaymentExtension)\z/.freeze

def self.enabled?
!(defined?($RNMParticleDisableSPM) && $RNMParticleDisableSPM)
end

def self.core_version
(defined?($RNMParticleSPMCoreVersion) && $RNMParticleSPMCoreVersion) || DEFAULT_CORE_VERSION
end

# $RNMParticleSPMKits as { url:, product:, version: } hashes. mParticle kits are released in
# lockstep with the core, so a kit without a version gets the core's.
def self.kits
entries = Array(defined?($RNMParticleSPMKits) ? $RNMParticleSPMKits : nil)
names = entries.grep(String)
names.each do |name|
included = Array(KITS.dig(name, 'includes')) & names
next if included.empty?

raise Pod::Informative,
"[mParticle] $RNMParticleSPMKits lists #{name} and #{included.join(', ')}, but #{name} " \
"already includes #{included.join(', ')}. List only #{name}."
end
entries.map { |entry| resolve_kit(entry) }
end

def self.resolve_kit(entry)
if entry.is_a?(String)
kit = KITS[entry]
unless kit
raise Pod::Informative,
"[mParticle] $RNMParticleSPMKits: unknown kit #{entry.inspect}. Use a pod name from " \
'react-native-mparticle/ios/mparticle_spm_kits.json, or { url:, product:, version: } ' \
'for any other kit.'
end
return { url: kit['url'], product: kit['product'], version: core_version }
end

kit = entry.is_a?(Hash) ? entry.transform_keys(&:to_sym) : {}
unless kit[:url] && kit[:product]
raise Pod::Informative,
"[mParticle] $RNMParticleSPMKits: #{entry.inspect} must be a kit pod name, or a hash " \
'with url: and product: (and optionally version:).'
end
{ version: core_version }.merge(kit.slice(:url, :product, :version))
end

# Runs before CocoaPods resolves the platforms, which would otherwise fail on a tvOS target with a
# generic "not compatible" error.
def self.check_podfile!(podfile)
return unless podfile

tvos = podfile.target_definition_list.select do |definition|
definition.platform&.name == :tvos &&
definition.dependencies.any? { |dependency| dependency.root_name == 'react-native-mparticle' }
end
return if tvos.empty?

raise Pod::Informative,
"[mParticle] Swift Package Manager mode is iOS only, but #{tvos.map(&:name).join(', ')} " \
'targets tvOS. Set $RNMParticleDisableSPM = true at the top of the Podfile to take the ' \
'mParticle SDKs from CocoaPods for the whole Podfile.'
end

def self.guard!(installer)
conflicts = installer.pod_targets.map(&:pod_name).uniq.grep(CONFLICTING_PODS)
return if conflicts.empty?

raise Pod::Informative,
"[mParticle] $RNMParticleUseSPM is set, but these pods would add a second copy of the " \
"mParticle/Rokt SDKs next to the Swift packages: #{conflicts.sort.join(', ')}. Remove them " \
"(including any mParticle pre_install dynamic-framework hook entries) and add kits via " \
"mparticle_spm_post_install(installer, kits: [...]) instead."
'[mParticle] This package takes the mParticle SDKs from Swift Package Manager, but these ' \
"pods would add a second copy: #{conflicts.sort.join(', ')}. Either remove them (and their " \
'entries in any pre_install dynamic-framework hook) and list the kits in ' \
'$RNMParticleSPMKits, or set $RNMParticleDisableSPM = true at the top of the Podfile to stay ' \
'on CocoaPods.'
end

# Only iOS apps that use this pod: other apps in the same Podfile must not get the SDKs.
Expand Down Expand Up @@ -76,20 +143,65 @@ def self.add_package(project, target, url:, product:, version:)
target.frameworks_build_phase.files << build_file
true
end
end

def mparticle_spm_post_install(installer, core_version: MParticleSPM::DEFAULT_CORE_VERSION, kits: [])
MParticleSPM.guard!(installer)
# 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|
added = MParticleSPM.add_package(project, target, **pkg.slice(:url, :product, :version))
Pod::UI.puts "[mParticle] #{target.name} <- #{pkg[:product]} #{pkg[:version]}".green if added
def self.link_packages(installer)
packages = [{ url: CORE_URL, product: CORE_PRODUCT, version: core_version }] + kits
touched = {}
application_targets(installer).each do |project, target|
packages.each do |pkg|
added = add_package(project, target, **pkg.slice(:url, :product, :version))
Pod::UI.puts "[mParticle] #{target.name} <- #{pkg[:product]} #{pkg[:version]}".green if added
end
touched[project.path.to_s] = project
end
touched[project.path.to_s] = project
touched.each_value(&:save)
end
touched.each_value(&:save)

# With the opt-out, the CocoaPods SDK and any leftover Swift package would both be linked. Kits
# outside the table are caught by their `mParticle-` product name.
def self.warn_leftover_packages(installer)
products = [CORE_PRODUCT] + KITS.values.map { |kit| kit['product'] }
application_targets(installer).each do |_, target|
leftover = target.package_product_dependencies.map(&:product_name).select do |product|
products.include?(product) || product.start_with?('mParticle-')
end
next if leftover.empty?

Pod::UI.warn "[mParticle] $RNMParticleDisableSPM is set, but #{target.name} still links the " \
"Swift packages #{leftover.join(', ')}. Remove them from the target, or the app " \
'contains two copies of the SDK.'
end
end

# Prepended to Pod::Installer when the Podfile evaluates the podspec (React Native's
# use_native_modules! does), before the install starts, so no Podfile hook is needed.
module InstallerHooks
private

def resolve_dependencies
MParticleSPM.check_podfile!(podfile) if MParticleSPM.enabled?
super
end

def validate_targets
if MParticleSPM.enabled?
MParticleSPM.guard!(self)
MParticleSPM.kits
end
super
end

def run_podfile_post_install_hooks
super
if MParticleSPM.enabled?
MParticleSPM.link_packages(self)
else
MParticleSPM.warn_leftover_packages(self)
end
end
end
end

if defined?(Pod::Installer) && !Pod::Installer.ancestors.include?(MParticleSPM::InstallerHooks)
Pod::Installer.prepend(MParticleSPM::InstallerHooks)
end
Loading
Loading