Skip to content

Commit cff14c5

Browse files
BrandonStalnakerthomson-tclaude
authored
feat: Support Shoppable ads and v9 of mParticle iOS SDK (#311)
Co-authored-by: Thomson Thomas <thomson.thomas@rokt.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 441fc7e commit cff14c5

22 files changed

Lines changed: 403 additions & 189 deletions

File tree

.github/workflows/pull-request.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ jobs:
2525
steps:
2626
- name: Checkout
2727
uses: actions/checkout@v6
28+
- uses: actions/setup-node@v6
29+
with:
30+
node-version: 24
31+
cache: yarn
32+
cache-dependency-path: yarn.lock
33+
- name: Install node modules
34+
run: yarn install --frozen-lockfile
2835
- name: Trunk Check
2936
uses: trunk-io/trunk-action@v1
3037

.trunk/trunk.yaml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ runtimes:
88
enabled:
99
- go@1.19.5
1010
- java@13.0.11
11-
- node@18.12.1
11+
# markdownlint 0.48+ / string-width need Node 20+ (regex v flag)
12+
- node@20.18.0
1213
- python@3.10.8
1314
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
1415
lint:
@@ -40,10 +41,19 @@ lint:
4041
- actionlint@1.6.9
4142
- checkov@3.2.507
4243
- dotenv-linter@3.3.0
43-
- eslint@10.0.2
44+
# ESLint 9+ defaults to flat config only; this repo uses .eslintrc.js (ESLint 8 style).
45+
# Trunk runs ESLint in an isolated env without the repo's node_modules; bundle the same
46+
# plugins/parser as package.json so @typescript-eslint/* resolves (CI + local).
47+
- eslint@8.57.1:
48+
packages:
49+
- '@typescript-eslint/eslint-plugin@5.62.0'
50+
- '@typescript-eslint/parser@5.62.0'
51+
- 'eslint-config-prettier@8.10.0'
52+
- 'eslint-plugin-prettier@4.2.1'
4453
- git-diff-check
4554
- ktlint@0.43.2
46-
- markdownlint@0.48.0
55+
# 0.48+ pulls string-width that requires Node 20+ for regex /v; Trunk's runner uses Node 18.
56+
- markdownlint@0.39.0
4757
- mparticle-api-key-check
4858
- osv-scanner@1.3.6
4959
- oxipng@7.0.0

ExpoTestApp/App.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,44 @@ export default function App() {
219219
const handleRoktBottomSheet = () =>
220220
handleRoktSelectPlacements('MSDKBottomSheetLayout');
221221

222+
const handleRoktShoppableAds = () => {
223+
const attributes = {
224+
country: 'US',
225+
shippingstate: 'NY',
226+
shippingzipcode: '10001',
227+
firstname: 'Jenny',
228+
stripeApplePayAvailable: 'true',
229+
last4digits: '4444',
230+
shippingaddress1: '123 Main St',
231+
colormode: 'LIGHT',
232+
billingzipcode: '07762',
233+
paymenttype: 'ApplePay',
234+
shippingcountry: 'US',
235+
sandbox: 'true',
236+
shippingaddress2: 'Apt 4B',
237+
confirmationref: 'ORD-12345',
238+
shippingcity: 'New York',
239+
newToApplePay: 'false',
240+
applePayCapabilities: 'true',
241+
lastname: 'Smith',
242+
email: 'jenny.smith@example.com',
243+
};
244+
245+
const config = MParticle.Rokt.createRoktConfig('system');
246+
247+
addLog('Rokt: Calling selectShoppableAds');
248+
249+
MParticle.Rokt.selectShoppableAds('StgRoktShoppableAds', attributes, config)
250+
.then((result: any) => {
251+
addLog(`Rokt selectShoppableAds success: ${JSON.stringify(result)}`);
252+
setStatus('Rokt: Shoppable Ads loaded');
253+
})
254+
.catch((error: any) => {
255+
addLog(`Rokt selectShoppableAds error: ${JSON.stringify(error)}`);
256+
setStatus(`Rokt error: ${error.message || 'Unknown error'}`);
257+
});
258+
};
259+
222260
return (
223261
<SafeAreaView style={styles.container}>
224262
<StatusBar barStyle="dark-content" />
@@ -323,6 +361,13 @@ export default function App() {
323361
>
324362
<Text style={styles.buttonText}>Bottom Sheet</Text>
325363
</TouchableOpacity>
364+
365+
<TouchableOpacity
366+
style={[styles.button, styles.roktButtonAlt]}
367+
onPress={handleRoktShoppableAds}
368+
>
369+
<Text style={styles.buttonText}>Shoppable Ads</Text>
370+
</TouchableOpacity>
326371
</View>
327372

328373
{/* Rokt Embedded Placeholder */}

ExpoTestApp/README.md

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,68 @@ The app also includes Rokt placement testing via the mParticle Rokt kit:
8080
- **Embedded**: Loads an embedded Rokt placement that renders in-line within the app content. The placement appears in the designated placeholder area below the buttons.
8181
- **Overlay**: Loads a full-screen overlay Rokt placement that appears on top of the app content.
8282
- **Bottom Sheet**: Loads a bottom sheet Rokt placement that slides up from the bottom of the screen.
83+
- **Shoppable Ads**: Calls `MParticle.Rokt.selectShoppableAds` with a staging placement identifier and checkout-style attributes (see implementation guide below).
8384

8485
The Rokt section also demonstrates:
8586

8687
- Platform-specific attributes (iOS vs Android configurations)
8788
- Rokt event listeners for callbacks and placement events
8889
- Using `RoktLayoutView` as an embedded placeholder component
8990

91+
### Implementation guide: Shoppable Ads (`selectShoppableAds`) and iOS payment extensions
92+
93+
This mirrors the recent SDK work (Shoppable Ads API on iOS and the Expo test app wiring) and how to pair it with native payment registration.
94+
95+
#### JavaScript: `selectShoppableAds`
96+
97+
Use `MParticle.Rokt.selectShoppableAds(identifier, attributes, roktConfig?)` when you need the Shoppable Ads experience instead of `selectPlacements`.
98+
99+
- **identifier**: Rokt page / placement identifier configured for your account (the Expo test app uses a staging example such as `StgRoktShoppableAds`; replace with your production identifier).
100+
- **attributes**: String key/value pairs passed to Rokt (shipping, billing, payment hints, sandbox flags, etc.). The demo in `App.tsx` includes fields like `country`, `shippingstate`, `paymenttype`, `stripeApplePayAvailable`, `applePayCapabilities`, and `sandbox`—adjust to match your integration and Rokt’s attribute contract.
101+
- **roktConfig**: Optional; the demo uses `MParticle.Rokt.createRoktConfig('system')` for color mode. Add a cache config if you use caching elsewhere.
102+
103+
Example (same pattern as `ExpoTestApp/App.tsx`):
104+
105+
```javascript
106+
const config = MParticle.Rokt.createRoktConfig('system');
107+
108+
MParticle.Rokt.selectShoppableAds('YOUR_PLACEMENT_ID', attributes, config)
109+
.then(() => {
110+
/* success */
111+
})
112+
.catch(error => {
113+
/* handle */
114+
});
115+
```
116+
117+
Listen for `RoktCallback` and `RoktEvents` on `RoktEventManager` to observe load/unload and Shoppable Ads–related events emitted by the native bridge.
118+
119+
**Android:** `selectShoppableAds` is not implemented on Android yet; the native module logs a warning and does not run the Shoppable Ads flow. Plan for iOS-only behavior until Android support ships.
120+
121+
#### iOS native: `RoktStripePaymentExtension` (payment extensions)
122+
123+
Shoppable Ads flows that use Apple Pay / Stripe integration expect a **payment extension** to be registered on mParticle’s Rokt interface after the SDK starts.
124+
125+
In `ios/MParticleExpoTest/AppDelegate.swift`, the test app:
126+
127+
1. Imports the Stripe payment extension module provided with the Rokt / kit stack: `import RoktStripePaymentExtension`.
128+
2. After `MParticle.sharedInstance().start(with: mParticleOptions)`, constructs `RoktStripePaymentExtension(applePayMerchantId: "...")` with your **Apple Pay merchant ID** (replace `merchant.dummy` with your real `merchant.*` identifier from Apple Developer).
129+
3. Registers it: `MParticle.sharedInstance().rokt.register(paymentExtension)`.
130+
131+
```swift
132+
import RoktStripePaymentExtension
133+
134+
// After MParticle.sharedInstance().start(with: mParticleOptions):
135+
if let paymentExtension = RoktStripePaymentExtension(applePayMerchantId: "merchant.your.id") {
136+
MParticle.sharedInstance().rokt.register(paymentExtension)
137+
}
138+
```
139+
140+
**Important:**
141+
142+
- The Expo config plugin **does not** generate the payment extension block today. After `expo prebuild`, add or merge this code into `AppDelegate.swift` (inside the same app launch path as mParticle init). If you regenerate native projects with `--clean`, re-apply this snippet.
143+
- Ensure the **mParticle Rokt kit** (and transitive Rokt dependencies) are installed so `RoktStripePaymentExtension` resolves—same as configuring `iosKits`: `["mParticle-Rokt"]` in `app.json`.
144+
90145
All activity is logged in the Activity Log section at the bottom of the screen.
91146

92147
## Verifying Plugin Integration
@@ -116,6 +171,8 @@ Check `ios/MParticleExpoTest/AppDelegate.swift` for:
116171
MParticle.sharedInstance().start(with: mParticleOptions)
117172
```
118173

174+
For Shoppable Ads with Apple Pay / Stripe, you may also need to register `RoktStripePaymentExtension` after `start`—see **Implementation guide: Shoppable Ads (`selectShoppableAds`) and iOS payment extensions** above.
175+
119176
#### Objective-C AppDelegate (Legacy)
120177

121178
For older Expo SDK versions, check `ios/MParticleExpoTest/AppDelegate.mm` for:
@@ -224,16 +281,16 @@ dependencies {
224281

225282
## Plugin Configuration Options
226283

227-
| Option | Type | Description |
228-
|--------|------|-------------|
229-
| `iosApiKey` | string | mParticle iOS API key |
230-
| `iosApiSecret` | string | mParticle iOS API secret |
231-
| `androidApiKey` | string | mParticle Android API key |
232-
| `androidApiSecret` | string | mParticle Android API secret |
233-
| `logLevel` | string | Log level: `none`, `error`, `warning`, `debug`, `verbose` |
234-
| `environment` | string | Environment: `development`, `production`, `autoDetect` |
235-
| `useEmptyIdentifyRequest` | boolean | Initialize with empty identify request (default: true) |
236-
| `dataPlanId` | string | Data plan ID for validation |
237-
| `dataPlanVersion` | number | Data plan version |
238-
| `iosKits` | string[] | iOS kit pod names (e.g., `["mParticle-Rokt"]`) |
239-
| `androidKits` | string[] | Android kit dependencies (e.g., `["android-rokt-kit"]`) |
284+
| Option | Type | Description |
285+
| ------------------------- | -------- | --------------------------------------------------------- |
286+
| `iosApiKey` | string | mParticle iOS API key |
287+
| `iosApiSecret` | string | mParticle iOS API secret |
288+
| `androidApiKey` | string | mParticle Android API key |
289+
| `androidApiSecret` | string | mParticle Android API secret |
290+
| `logLevel` | string | Log level: `none`, `error`, `warning`, `debug`, `verbose` |
291+
| `environment` | string | Environment: `development`, `production`, `autoDetect` |
292+
| `useEmptyIdentifyRequest` | boolean | Initialize with empty identify request (default: true) |
293+
| `dataPlanId` | string | Data plan ID for validation |
294+
| `dataPlanVersion` | number | Data plan version |
295+
| `iosKits` | string[] | iOS kit pod names (e.g., `["mParticle-Rokt"]`) |
296+
| `androidKits` | string[] | Android kit dependencies (e.g., `["android-rokt-kit"]`) |

ExpoTestApp/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
"prebuild": "expo prebuild --clean"
1111
},
1212
"dependencies": {
13-
"react-native-mparticle": "file:../react-native-mparticle-latest.tgz",
1413
"expo": "~54.0.25",
14+
"expo-build-properties": "~1.0.10",
1515
"expo-dev-client": "~6.0.16",
1616
"expo-status-bar": "~3.0.8",
1717
"react": "19.1.0",
18-
"react-native": "0.81.5"
18+
"react-native": "0.81.5",
19+
"react-native-mparticle": "file:.."
1920
},
2021
"devDependencies": {
2122
"@babel/core": "^7.25.2",

android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.mparticle.MpRoktEventCallback
1616
import com.mparticle.RoktEvent
1717
import com.mparticle.UnloadReasons
1818
import com.mparticle.WrapperSdk
19+
import com.mparticle.internal.Logger
1920
import com.mparticle.rokt.CacheConfig
2021
import com.mparticle.rokt.RoktConfig
2122
import kotlinx.coroutines.Job
@@ -39,6 +40,14 @@ class MPRoktModuleImpl(
3940

4041
fun getName(): String = MODULE_NAME
4142

43+
fun selectShoppableAds(
44+
identifier: String,
45+
attributes: ReadableMap?,
46+
roktConfig: ReadableMap?,
47+
) {
48+
Logger.warning("selectShoppableAds is not yet supported on Android")
49+
}
50+
4251
fun purchaseFinalized(
4352
placementId: String,
4453
catalogItemId: String,

android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,15 @@ import com.facebook.react.bridge.ReadableType
77
import com.facebook.react.bridge.UiThreadUtil
88
import com.facebook.react.uimanager.UIManagerHelper
99
import com.mparticle.MParticle
10-
import com.mparticle.WrapperSdk
10+
import com.mparticle.internal.Logger
1111
import com.mparticle.react.NativeMPRoktSpec
1212
import com.mparticle.rokt.RoktEmbeddedView
13-
import com.mparticle.internal.Logger
1413
import java.lang.ref.WeakReference
1514
import java.util.concurrent.CountDownLatch
1615

1716
class MPRoktModule(
1817
private val reactContext: ReactApplicationContext,
1918
) : NativeMPRoktSpec(reactContext) {
20-
2119
private val impl = MPRoktModuleImpl(reactContext)
2220

2321
override fun getName(): String = impl.getName()
@@ -52,6 +50,15 @@ class MPRoktModule(
5250
)
5351
}
5452

53+
@ReactMethod
54+
override fun selectShoppableAds(
55+
identifier: String,
56+
attributes: ReadableMap?,
57+
roktConfig: ReadableMap?,
58+
) {
59+
impl.selectShoppableAds(identifier, attributes, roktConfig)
60+
}
61+
5562
@ReactMethod
5663
override fun purchaseFinalized(
5764
placementId: String,
@@ -61,7 +68,6 @@ class MPRoktModule(
6168
impl.purchaseFinalized(placementId, catalogItemId, success)
6269
}
6370

64-
6571
/**
6672
* Process placeholders from ReadableMap to a map of Widgets for use with Rokt.
6773
* This method handles the Fabric-specific view resolution.
@@ -83,8 +89,9 @@ class MPRoktModule(
8389
// Get the tag value as an integer
8490
val reactTag =
8591
when {
86-
placeholders.getType(key) == ReadableType.Number ->
92+
placeholders.getType(key) == ReadableType.Number -> {
8793
placeholders.getDouble(key).toInt()
94+
}
8895

8996
else -> {
9097
Logger.warning("Invalid view tag for key: $key")

android/src/oldarch/java/com/mparticle/react/NativeMPRoktSpec.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ abstract class NativeMPRoktSpec(
2121
fontFilesMap: ReadableMap?,
2222
)
2323

24+
abstract fun selectShoppableAds(
25+
identifier: String,
26+
attributes: ReadableMap?,
27+
roktConfig: ReadableMap?,
28+
)
29+
2430
abstract fun purchaseFinalized(
2531
placementId: String,
2632
catalogItemId: String,

android/src/oldarch/java/com/mparticle/react/rokt/MPRoktModule.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ class MPRoktModule(
4848
}
4949
}
5050

51+
@ReactMethod
52+
override fun selectShoppableAds(
53+
identifier: String,
54+
attributes: ReadableMap?,
55+
roktConfig: ReadableMap?,
56+
) {
57+
impl.selectShoppableAds(identifier, attributes, roktConfig)
58+
}
59+
5160
@ReactMethod
5261
override fun purchaseFinalized(
5362
placementId: String,

0 commit comments

Comments
 (0)