Skip to content
Merged
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
22 changes: 4 additions & 18 deletions ExpoTestApp/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* It demonstrates SDK initialization, event logging, and Rokt placements.
*/

import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import {
SafeAreaView,
ScrollView,
Expand All @@ -16,7 +16,6 @@ import {
TouchableOpacity,
View,
Platform,
findNodeHandle,
NativeEventEmitter,
} from 'react-native';
import MParticle from 'react-native-mparticle';
Expand All @@ -38,9 +37,6 @@ export default function App() {
const [status, setStatus] = useState('SDK initialized via native code');
const [logs, setLogs] = useState<string[]>([]);

// Ref for Rokt embedded placeholder
const roktPlaceholderRef = useRef<any>(null);

const addLog = (message: string) => {
const timestamp = new Date().toLocaleTimeString();
setLogs(prev => [`[${timestamp}] ${message}`, ...prev].slice(0, 20));
Expand Down Expand Up @@ -194,18 +190,11 @@ export default function App() {
const cacheConfig = MParticle.Rokt.createCacheConfig(30, attributes);
const config = MParticle.Rokt.createRoktConfig('system', cacheConfig);

// Build placeholder map for embedded placements
const placeholderMap: { [key: string]: number | null } = {};
const nodeHandle = findNodeHandle(roktPlaceholderRef.current);
if (nodeHandle !== null) {
placeholderMap['Location1'] = nodeHandle;
}

// Call selectPlacements
// Call selectPlacements, embedding into the mounted <RoktLayoutView placeholderName="Location1" />
MParticle.Rokt.selectPlacements(
identifier,
attributes,
placeholderMap,
['Location1'],
config,
undefined
)
Expand Down Expand Up @@ -421,10 +410,7 @@ export default function App() {
<Text style={styles.placeholderLabel}>
Embedded Placement Area:
</Text>
<RoktLayoutView
ref={roktPlaceholderRef}
placeholderName="Location1"
/>
<RoktLayoutView placeholderName="Location1" />
</View>
</View>

Expand Down
17 changes: 10 additions & 7 deletions ExpoTestApp/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.mparticle.expotestapp"
Expand All @@ -27,11 +21,20 @@
"favicon": "./assets/favicon.png"
},
"plugins": [
[
"expo-splash-screen",
{
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
}
],
[
"expo-build-properties",
{
"ios": {
"deploymentTarget": "15.6"
"deploymentTarget": "16.4",
"enableSceneSupport": true
}
}
],
Expand Down
20 changes: 12 additions & 8 deletions ExpoTestApp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,22 @@
"prebuild": "expo prebuild --clean"
},
"dependencies": {
"expo": "~54.0.25",
"expo-build-properties": "~1.0.10",
"expo-dev-client": "~6.0.16",
"expo-status-bar": "~3.0.8",
"react": "19.1.0",
"react-native": "0.81.5",
"expo": "~57.0.25",
"expo-build-properties": "~57.0.22",
"expo-dev-client": "~57.0.19",
"expo-splash-screen": "~57.0.9",
"expo-status-bar": "~57.0.1",
"react": "19.2.3",
"react-native": "0.86.3",
"react-native-mparticle": "file:.."
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/react": "~19.1.0",
"typescript": "~5.9.2"
"@types/react": "~19.2.4",
"typescript": "~6.0.3"
},
"engines": {
"node": ">=22.13.0"
},
"private": true
}
63 changes: 63 additions & 0 deletions MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,69 @@

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

## Migrating embedded placements to placeholder names

`MParticle.Rokt.selectPlacements` can find each embedded `RoktLayoutView` by its
`placeholderName`, so apps no longer need a ref, `findNodeHandle`, or to wait for
the view to mount before calling it. This is not a breaking change: the map of
placeholder names to React tags still works. We recommend moving to names the
next time you touch the integration.

Before, tag-based:

```jsx
import { findNodeHandle } from 'react-native';

const placeholderRef = useRef(null);

const onPlaceholderLayout = () => {
MParticle.Rokt.selectPlacements('MSDKEmbeddedLayout', attributes, {
Location1: findNodeHandle(placeholderRef.current),
});
};

return (
<View onLayout={onPlaceholderLayout}>
<MParticle.RoktLayoutView
ref={placeholderRef}
placeholderName="Location1"
/>
</View>
);
```

After, name-based:

```jsx
useEffect(() => {
MParticle.Rokt.selectPlacements('MSDKEmbeddedLayout', attributes, [
'Location1',
]);
}, []);

return <MParticle.RoktLayoutView placeholderName="Location1" />;
```

To migrate, remove the ref, the `findNodeHandle` import and any `onLayout`
handler or timer used to delay the call, then pass an array of placeholder
names. Each name must match the `placeholderName` of a `RoktLayoutView`, the same
key the map uses today.

### Behavior changes to check

- **A named placeholder that has not mounted yet delays the request by up to 2
seconds.** The SDK waits for the view, then calls Rokt with the views it has.
A misspelled or never-rendered name therefore arrives 2 seconds late, and is
logged as `Cannot resolve placeholder`.
- **`null` in the map form is resolved by name.** `findNodeHandle` returns `null`
before the view mounts; that placeholder was skipped before and is now looked
up by its key.
- **A waiting call can end in `PlacementFailure`.** If `close()` runs, or a newer
call with the same identifier replaces it, before its placeholders mount, the
waiting call emits `PlacementFailure` instead of being dropped silently.

The map form is planned for removal in a future major version.

## Migrating from versions < 3.0.0

`3.0.0` moved iOS to the mParticle Apple SDK **9.x**. Later 3.x releases raised
Expand Down
45 changes: 26 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,16 @@ The `startWithOptions` method requires an options argument containing your key a

For more help, see [the iOS set up docs](https://docs.mparticle.com/developers/sdk/ios/getting-started/#create-an-input).

> **React Native 0.77+ requires a Fabric dependency provider.** Set it in `application:didFinishLaunchingWithOptions:` before starting mParticle. Without it no third-party Fabric component is registered, so `<RoktLayoutView>` mounts as `RCTUnimplementedViewComponentView` and embedded placements never appear. This fails at runtime, not at build time. See `sample/ios/MParticleSample/AppDelegate.mm`.
> **React Native 0.77+ requires a Fabric dependency provider.** Set it in `application:didFinishLaunchingWithOptions:` before starting mParticle. Without it no third-party Fabric component is registered, so `<RoktLayoutView>` mounts as `RCTUnimplementedViewComponentView` and embedded placements never appear. This fails at runtime, not at build time. See `sample/ios/MParticleSample/AppDelegate.swift`.
>
> ```objective-c
> #import <ReactAppDependencyProvider/RCTAppDependencyProvider.h>
> ```swift
> import ReactAppDependencyProvider
>
> self.dependencyProvider = [RCTAppDependencyProvider new];
> let delegate = ReactNativeDelegate()
> delegate.dependencyProvider = RCTAppDependencyProvider()
>
> reactNativeDelegate = delegate
> reactNativeFactory = RCTReactNativeFactory(delegate: delegate)
> ```

> **If you install no iOS kit, declare the umbrella pod.** `mParticle-Apple-SDK` is now a thin Swift umbrella over `mParticle-Apple-SDK-ObjC`, and this wrapper depends on the ObjC pod directly — so the umbrella is installed only when something else declares it, as `mParticle-Rokt` 9.x does. Without a kit, add `pod 'mParticle-Apple-SDK', '>= 9.2.2', '< 10.0'` (matching this library's own floor) for `import mParticle_Apple_SDK` to resolve, or import `mParticle_Apple_SDK_ObjC` instead.
Expand Down Expand Up @@ -687,26 +691,29 @@ the native call, and `getSessionId` resolves with the current session ID (or
`null`), but no Rokt method reports success or failure through its promise —
watch the events below for the outcome.

For embedded placements, render `RoktLayoutView` and pass its node handle in the
`placeholders` map, keyed by the same `placeholderName`:
For embedded placements, render `RoktLayoutView` and pass its `placeholderName`
in the `placeholders` array. The view does not need to be mounted when you call
`selectPlacements`: the SDK waits up to 2 seconds for each named placeholder, so
calling it from `useEffect` is fine. If a call is still waiting when `close()`
runs, or when a newer call with the same identifier replaces it, that call emits
`PlacementFailure` instead.

```jsx
<MParticle.RoktLayoutView ref={this.placeholder1} placeholderName="Location1" />
```

```js
import { findNodeHandle } from 'react-native';

const placeholders = { Location1: findNodeHandle(this.placeholder1.current) };
useEffect(() => {
MParticle.Rokt.selectPlacements(
'MSDKEmbeddedLayout',
attributes,
['Location1'],
config
);
}, []);

MParticle.Rokt.selectPlacements(
'MSDKEmbeddedLayout',
attributes,
placeholders,
config
);
return <MParticle.RoktLayoutView placeholderName="Location1" />;
```

The earlier form, a map of `placeholderName` to `findNodeHandle(ref)`, is still
supported: `{ Location1: findNodeHandle(this.placeholder1.current) }`.

| Method | Notes |
| ----------------------------------------------------- | ----------------------------------------------------- |
| `selectShoppableAds(identifier, attributes, config?)` | iOS only — logs a warning and does nothing on Android |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,65 @@ class MPRoktModuleImpl(
fun close(promise: Promise) {
// rokt.close() dismisses/detaches Compose overlay views, which must happen on the main thread.
UiThreadUtil.runOnUiThread {
RoktPlaceholderRegistry.cancelWaits()
MParticle.getInstance()?.rokt?.close()
promise.resolve(null)
}
}

/**
* Runs [select] on the UI thread once every placeholder named for name-based resolution has
* mounted, or after [PLACEHOLDER_MOUNT_TIMEOUT_MS]. A placeholder may not be mounted yet when
* selectPlacements arrives, e.g. when it is called from the same useEffect that rendered it:
* Fabric creates views on the next frame. Legacy react tags are never waited for.
* Must be called on the UI thread.
*/
fun whenPlaceholdersMounted(
identifier: String,
placeholders: ReadableMap?,
select: () -> Unit,
) {
val pending = unmountedPlaceholderNames(placeholders)
if (pending.isEmpty()) {
select()
return
}
Logger.debug("Waiting up to ${PLACEHOLDER_MOUNT_TIMEOUT_MS}ms for placeholder(s) to mount: $pending")
RoktPlaceholderRegistry.awaitNames(
identifier,
pending,
PLACEHOLDER_MOUNT_TIMEOUT_MS,
onDiscard = { sendPlacementFailure(identifier) },
onReady = select,
)
}

// Replaced by a newer call with the same identifier, or cancelled by close(): the SDK is never
// called, so report the failure in the shape the event listener sends for the SDK's own
// PlacementFailure (no placementId), keeping the app from waiting on an event that never comes.
private fun sendPlacementFailure(viewName: String) {
Logger.debug("Pending selectPlacements dropped for: $viewName")
val params = Arguments.createMap()
params.putString("event", "PlacementFailure")
params.putString("viewName", viewName)
sendEvent(reactContext, "RoktEvents", params)
}

// Names passed for name-based resolution (a non-positive value) that have no mounted view yet.
internal fun unmountedPlaceholderNames(placeholders: ReadableMap?): List<String> {
if (placeholders == null) return emptyList()
val pending = mutableListOf<String>()
val iterator = placeholders.keySetIterator()
while (iterator.hasNextKey()) {
val key = iterator.nextKey()
val isReactTag = placeholders.getType(key) == ReadableType.Number && placeholders.getDouble(key) > 0
if (!isReactTag && RoktPlaceholderRegistry.lookup(key) == null) {
pending += key
}
}
return pending
}

fun setSessionId(
sessionId: String,
promise: Promise,
Expand Down Expand Up @@ -285,6 +339,9 @@ class MPRoktModuleImpl(
companion object {
const val MODULE_NAME = "RNMPRokt"

// How long selectPlacements waits for a named placeholder to mount before proceeding without it.
const val PLACEHOLDER_MOUNT_TIMEOUT_MS = 2000L

// Match iOS NSNumber stringValue: plain decimals, no trailing ".0", no scientific notation.
internal fun formatNumberAttribute(value: Double): String {
if (value.isNaN() || value.isInfinite()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ class RoktLayoutViewManagerImpl {
view: RoktEmbeddedView?,
value: String?,
) {
view?.tag = value
view ?: return
view.tag = value
RoktPlaceholderRegistry.register(view, value)
}

fun onDropViewInstance(view: RoktEmbeddedView) {
RoktPlaceholderRegistry.unregister(view)
}

private fun setUpWidgetListeners(widget: RoktEmbeddedView) {
Expand Down
Loading
Loading