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
29 changes: 27 additions & 2 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,20 @@ jobs:
run: ./gradlew assembleDebug

ios-sample-app:
# Keep this display name in sync with branch protection required checks ("iOS Sample App").
name: iOS Sample App
# The CocoaPods leg keeps the display name the "main" ruleset requires ("iOS Sample App").
name: ${{ matrix.check_name }}
runs-on: macos-15
strategy:
fail-fast: false
matrix:
include:
- ios_deps: cocoapods
check_name: iOS Sample App
- 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' }}
steps:
- name: Checkout
uses: actions/checkout@v7
Expand Down Expand Up @@ -178,6 +189,20 @@ jobs:
-only-testing:MParticleSampleTests/RNMParticleDuplicateSDKTests \
| bundle exec xcpretty -k

- name: Archive iOS sample app and check for a single copy of each SDK
working-directory: sample/ios
run: |
set -o pipefail && xcodebuild -workspace MParticleSample.xcworkspace \
-configuration Release \
-scheme MParticleSample \
-destination 'generic/platform=iOS' \
-derivedDataPath ios/build \
-archivePath ios/build/MParticleSample.xcarchive \
CODE_SIGNING_ALLOWED=NO \
archive \
| bundle exec xcpretty -k
../../scripts/ios/check-single-copy.sh ios/build/MParticleSample.xcarchive/Products/Applications/MParticleSample.app

pr-notify:
if: >
github.event_name == 'pull_request' &&
Expand Down
16 changes: 16 additions & 0 deletions MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

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

## Moving an iOS app to Swift Package Manager mode

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.

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`.

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`.

## Migrating embedded placements to placeholder names

`MParticle.Rokt.selectPlacements` can find each embedded `RoktLayoutView` by its
Expand Down
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,49 @@ npm start

and build your workspace from xCode.

### Swift Package Manager
### Swift Package Manager (opt-in)

Take the mParticle SDK and its kits from one dependency manager only. If the core comes from CocoaPods (through this package) and a kit such as `mParticle-Rokt` is added to the app with Swift Package Manager, the app contains two copies of the SDK. It builds and archives without warnings, then crashes at runtime. Debug builds detect this and show a red box: `[mParticle] The mParticle SDK is loaded more than once`. To fix it, remove the mParticle and Rokt Swift packages from the app and add the kits with CocoaPods, as shown above.
By default this package takes the mParticle SDKs from CocoaPods. In Swift Package Manager mode, React Native and this package still install with CocoaPods, but the mParticle core SDK and its kits come from Swift Package Manager and are linked into your app target. Use it when you add kits with Swift Package Manager, or when you need mParticle SDK releases that are not published to CocoaPods. iOS only: `$RNMParticleUseSPM` applies to the whole Podfile, so a Podfile with a tvOS target that uses this package must stay on CocoaPods.

Every mParticle and Rokt SDK must then come from Swift Package Manager, and none from CocoaPods. If the core comes from one and a kit from the other, the app contains two copies of the SDK: it builds and archives without warnings, then crashes at runtime.

1. At the top of `ios/Podfile`, before any `target` block:

```ruby
$RNMParticleUseSPM = true
require_relative '../node_modules/react-native-mparticle/ios/mparticle_spm'
```

2. Remove the mParticle kit pods (for example `pod 'mParticle-Rokt'`) and the mParticle entries of the `pre_install` dynamic-framework hook shown above.

3. At the end of `post_install`, after `react_native_post_install(...)`, list your kits:

```ruby
mparticle_spm_post_install(installer, kits: [
{ url: 'https://github.com/mparticle-integrations/mp-apple-integration-rokt', product: 'mParticle-Rokt', version: '9.6.1' },
])
```

`core_version:` pins the core SDK; it defaults to the version this release was tested with.

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

**What `mparticle_spm_post_install` changes.** On each `pod install` it edits your app's `.xcodeproj`: it adds a Swift package reference for the mParticle core SDK and each kit, pinned to the exact version, and links each package's product into every iOS application target that uses this package. It prints every change (`[mParticle] MyApp <- mParticle-Rokt 9.6.1`) and running it again changes nothing. It stops `pod install` with an error if a pod would add a second copy of the SDKs. Commit the `.xcodeproj` change and `ios/<App>.xcworkspace/xcshareddata/swiftpm/Package.resolved`, so every build resolves the same versions.

**Linkage.** This package's pod is always built as a static framework in this mode, so it works with each CocoaPods linkage:

| Podfile linkage | Supported |
| --------------------------------------- | --------- |
| Static libraries (React Native default) | ✓ |
| `use_frameworks! :linkage => :static` | ✓ |
| `use_frameworks! :linkage => :dynamic` | ✓ |

**Troubleshooting.**

- `pod install` fails with `[mParticle] $RNMParticleUseSPM is set, but these pods would add a second copy of the mParticle/Rokt SDKs`: remove the pods it lists, including entries in a `pre_install` hook, and add those kits through `mparticle_spm_post_install` instead.
- `pod install` fails with `The Swift pod mParticle-Apple-SDK depends upon mParticle-Apple-SDK-ObjC, which does not define modules`: an mParticle kit pod such as `mParticle-Rokt` is still declared. CocoaPods reports this before the check above can run. Remove the kit pod and add the kit through `mparticle_spm_post_install`.
- The build fails with `[mParticle] $RNMParticleUseSPM is set but the mParticle-Apple-SDK Swift package is not linked into the app target`: `mparticle_spm_post_install` did not run, or the package was removed from the app target afterwards. Call it in `post_install` and run `pod install` again.
- A Debug build shows the red box `[mParticle] The mParticle SDK is loaded more than once`: the SDK comes from both CocoaPods and Swift Package Manager. Remove the mParticle and Rokt pods and Swift packages you added by hand, then set up one mode as described above.

## Android (Manual Setup)

Expand Down
93 changes: 93 additions & 0 deletions ios/mparticle_spm.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Opt-in Swift Package Manager mode for the mParticle SDKs (see README › Swift Package Manager).
#
# Podfile usage (bare React Native):
#
# $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
#
# 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.
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 = '9.6.1'.freeze
CONFLICTING_PODS = /\A(mParticle-.*|Rokt-Widget|RoktContracts|RoktUXHelper|DcuiSchema)\z/.freeze

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."
end

# Only iOS apps that use this pod: other apps in the same Podfile must not get the SDKs.
def self.application_targets(installer)
aggregates = installer.aggregate_targets.select do |aggregate|
aggregate.platform.name == :ios && aggregate.pod_targets.any? { |pod| pod.pod_name == 'react-native-mparticle' }
end
aggregates.flat_map do |aggregate|
aggregate.user_targets
.select { |t| t.product_type == 'com.apple.product-type.application' }
.map { |t| [aggregate.user_project, t] }
end.uniq { |project, target| [project.path.to_s, target.uuid] }
end

def self.add_package(project, target, url:, product:, version:)
ref = project.root_object.package_references.find do |r|
r.is_a?(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference) && r.repositoryURL == url
end
unless ref
ref = project.new(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference)
ref.repositoryURL = url
project.root_object.package_references << ref
end
requirement = { 'kind' => 'exactVersion', 'version' => version }
changed = ref.requirement != requirement
ref.requirement = requirement

return changed if target.package_product_dependencies.any? { |d| d.product_name == product }

dep = project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency)
dep.package = ref
dep.product_name = product
target.package_product_dependencies << dep
build_file = project.new(Xcodeproj::Project::Object::PBXBuildFile)
build_file.product_ref = dep
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)
packages = [{ url: MParticleSPM::CORE_URL, product: MParticleSPM::CORE_PRODUCT, version: core_version }] + kits
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
end
touched[project.path.to_s] = project
end
touched.each_value(&:save)
end
44 changes: 42 additions & 2 deletions react-native-mparticle.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,52 @@ Pod::Spec.new do |s|
s.source = { :git => "https://github.com/mParticle/react-native-mparticle.git", :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm,swift}"

# Opt-in Swift Package Manager mode: set `$RNMParticleUseSPM = true` at the top of the Podfile and
# call `mparticle_spm_post_install` (ios/mparticle_spm.rb) in post_install. When it is unset, this
# podspec resolves exactly as before.
use_spm = defined?($RNMParticleUseSPM) && $RNMParticleUseSPM

if use_spm
# The app target links mParticle and RoktContracts as Swift packages. This pod only compiles
# against their headers and never links them, so it is a static framework even under
# `use_frameworks! :linkage => :dynamic`.
s.static_framework = true
s.platforms = { :ios => ios_platform } # the Rokt kit Swift package is iOS-only

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that this needed addressing. $RNMParticleUseSPM applies to the whole Podfile, and the Rokt kit Swift package is iOS-only, so letting tvOS targets share a Podfile with SPM mode would need a separate design. Instead, 777b3d6 corrects the README: SPM mode is iOS-only, and a Podfile with a tvOS target that uses this package must stay on CocoaPods.

s.pod_target_xcconfig = {
'HEADER_SEARCH_PATHS' => '"$(DERIVED_FILE_DIR)/mParticleSPMInclude" "$(OBJROOT)/GeneratedModuleMaps-$(PLATFORM_NAME)"'
}
# Xcode writes a module map holding the absolute checkout path of each package, for build and
# archive alike, under OBJROOT. Linking that directory into this target's derived sources means no
# DerivedData or -clonedSourcePackagesDirPath layout is assumed. Declaring RoktContracts-Swift.h
# as an input orders this phase, and so this target's compile, after the RoktContracts package.
maps = '${OBJROOT}/GeneratedModuleMaps-${PLATFORM_NAME}'
s.script_phase = {
:name => '[mParticle] Locate Swift Package headers',
:execution_position => :before_compile,
:input_files => ["#{maps}/mParticle_Apple_SDK_ObjC.modulemap", "#{maps}/RoktContracts-Swift.h"],
:output_files => ['${DERIVED_FILE_DIR}/mParticleSPMInclude'],
:script => <<~'SH'
set -eu
MAP="${OBJROOT}/GeneratedModuleMaps-${PLATFORM_NAME}/mParticle_Apple_SDK_ObjC.modulemap"
INC=$(sed -n 's/^umbrella "\(.*\)"$/\1/p' "$MAP" 2>/dev/null || true)
if [ -z "$INC" ] || [ ! -d "$INC" ]; then
echo "error: [mParticle] \$RNMParticleUseSPM is set but the mParticle-Apple-SDK Swift package is not linked into the app target. Call mparticle_spm_post_install in your Podfile post_install."
exit 1
fi
mkdir -p "${DERIVED_FILE_DIR}"
ln -sfn "$INC" "${DERIVED_FILE_DIR}/mParticleSPMInclude"
SH
}
end

if respond_to?(:install_modules_dependencies, true)
install_modules_dependencies(s)
else
s.dependency "React-Core"
end

s.dependency 'mParticle-Apple-SDK-ObjC', '>= 9.2.2', '< 10.0'
s.dependency 'RoktContracts', '~> 2.0'
unless use_spm
s.dependency 'mParticle-Apple-SDK-ObjC', '>= 9.2.2', '< 10.0'
s.dependency 'RoktContracts', '~> 2.0'
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,8 @@
#import <React/RCTConvert.h>
#import "../../../ios/RNMParticle/RNMParticle.h"

// Match RNMParticle.mm / pod umbrella so tests compile against the same SDK the library uses.
#if defined(__has_include) && __has_include(<mParticle_Apple_SDK_ObjC/mParticle.h>)
#import <mParticle_Apple_SDK_ObjC/mParticle.h>
#elif defined(__has_include) && __has_include(<mParticle_Apple_SDK/mParticle.h>)
#import <mParticle_Apple_SDK/mParticle.h>
#else
#import <mParticle_Apple_SDK_ObjC/mParticle.h>
#endif
// The same SDK headers the library compiles against, in CocoaPods and Swift Package Manager mode.
#import "../../../ios/RNMParticle/RNMPSDKImports.h"

// Implemented on `RCTConvert` in `RNMParticle.mm` (react-native-mparticle pod).
// Parameter types mirror the implementations there.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#import <React/RCTBridgeModule.h>
#import <React/RCTInvalidating.h>
#import <React/RCTLog.h>
@import RoktContracts;
#import "../../../ios/RNMParticle/RNMPSDKImports.h"
#import "../../../ios/RNMParticle/RNMPRokt.h"
#import "../../../ios/RNMParticle/RoktPlaceholderRegistry.h"

Expand Down
34 changes: 28 additions & 6 deletions sample/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,21 @@ require Pod::Executable.execute_command('node', ['-p',
platform :ios, '15.6'
prepare_react_native_project!

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)
# MP_USE_SPM=1 takes the mParticle SDKs from Swift Package Manager instead of CocoaPods
# (README › Swift Package Manager). Must be set before `target`, which evaluates the podspec.
# It adds the Swift packages to MParticleSample.xcodeproj: discard that change before switching back
# to CocoaPods, or the app links both copies (Debug builds then show a red box).
$RNMParticleUseSPM = ENV['MP_USE_SPM'] == '1'
require_relative '../node_modules/react-native-mparticle/ios/mparticle_spm' if $RNMParticleUseSPM
ROKT_KIT = { url: 'https://github.com/mparticle-integrations/mp-apple-integration-rokt', product: 'mParticle-Rokt', version: '9.6.1' }.freeze

unless $RNMParticleUseSPM
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
Expand All @@ -38,7 +48,7 @@ target 'MParticleSample' do
# 5.3 resolves RoktUXHelper 1.0.0, which requires an exact `DcuiSchema` version, so the
# schema can no longer float out of sync with the RoktUXHelper sources (the desync that
# the previous 2.7.0 pin guarded against).
pod 'mParticle-Rokt', '>= 9.3.1', '< 10.0'
pod 'mParticle-Rokt', '>= 9.3.1', '< 10.0' unless $RNMParticleUseSPM

target 'MParticleSampleTests' do
inherit! :complete
Expand All @@ -53,5 +63,17 @@ target 'MParticleSample' do
:mac_catalyst_enabled => false,
# :ccache_enabled => true
)
if $RNMParticleUseSPM
mparticle_spm_post_install(installer, kits: [ROKT_KIT])
# The unit tests compile against the SDK headers but never link the SDK: the host app does.
# Point them at the headers the react-native-mparticle pod's script phase located.
spm_headers = '"$(OBJROOT)/Pods.build/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/react-native-mparticle.build/DerivedSources/mParticleSPMInclude" "$(OBJROOT)/GeneratedModuleMaps-$(PLATFORM_NAME)"'
installer.aggregate_targets.select { |t| t.name.end_with?('-MParticleSampleTests') }.each do |t|
t.xcconfigs.each do |build_config, xcconfig|
xcconfig.attributes['HEADER_SEARCH_PATHS'] = "#{xcconfig.attributes['HEADER_SEARCH_PATHS']} #{spm_headers}"
xcconfig.save_as(t.xcconfig_path(build_config))
end
end
end
end
end
Loading
Loading