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
7 changes: 7 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 24
cache: yarn
cache-dependency-path: yarn.lock
- name: Install node modules
run: yarn install --frozen-lockfile
- name: Trunk Check
uses: trunk-io/trunk-action@v1

Expand Down
16 changes: 13 additions & 3 deletions .trunk/trunk.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ runtimes:
enabled:
- go@1.19.5
- java@13.0.11
- node@18.12.1
# markdownlint 0.48+ / string-width need Node 20+ (regex v flag)
- node@20.18.0
- python@3.10.8
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
lint:
Expand Down Expand Up @@ -40,10 +41,19 @@ lint:
- actionlint@1.6.9
- checkov@3.2.507
- dotenv-linter@3.3.0
- eslint@10.0.2
# ESLint 9+ defaults to flat config only; this repo uses .eslintrc.js (ESLint 8 style).
# Trunk runs ESLint in an isolated env without the repo's node_modules; bundle the same
# plugins/parser as package.json so @typescript-eslint/* resolves (CI + local).
- eslint@8.57.1:
packages:
- '@typescript-eslint/eslint-plugin@5.62.0'
- '@typescript-eslint/parser@5.62.0'
- 'eslint-config-prettier@8.10.0'
- 'eslint-plugin-prettier@4.2.1'
- git-diff-check
- ktlint@0.43.2
- markdownlint@0.48.0
# 0.48+ pulls string-width that requires Node 20+ for regex /v; Trunk's runner uses Node 18.
- markdownlint@0.39.0
- mparticle-api-key-check
- osv-scanner@1.3.6
- oxipng@7.0.0
Expand Down
45 changes: 45 additions & 0 deletions ExpoTestApp/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,44 @@ export default function App() {
const handleRoktBottomSheet = () =>
handleRoktSelectPlacements('MSDKBottomSheetLayout');

const handleRoktShoppableAds = () => {
const attributes = {
country: 'US',
shippingstate: 'NY',
shippingzipcode: '10001',
firstname: 'Jenny',
stripeApplePayAvailable: 'true',
last4digits: '4444',
shippingaddress1: '123 Main St',
colormode: 'LIGHT',
billingzipcode: '07762',
paymenttype: 'ApplePay',
shippingcountry: 'US',
sandbox: 'true',
shippingaddress2: 'Apt 4B',
confirmationref: 'ORD-12345',
shippingcity: 'New York',
newToApplePay: 'false',
applePayCapabilities: 'true',
lastname: 'Smith',
email: 'jenny.smith@example.com',
};

const config = MParticle.Rokt.createRoktConfig('system');

addLog('Rokt: Calling selectShoppableAds');

MParticle.Rokt.selectShoppableAds('StgRoktShoppableAds', attributes, config)
.then((result: any) => {
addLog(`Rokt selectShoppableAds success: ${JSON.stringify(result)}`);
setStatus('Rokt: Shoppable Ads loaded');
})
.catch((error: any) => {
addLog(`Rokt selectShoppableAds error: ${JSON.stringify(error)}`);
setStatus(`Rokt error: ${error.message || 'Unknown error'}`);
});
};

return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" />
Expand Down Expand Up @@ -323,6 +361,13 @@ export default function App() {
>
<Text style={styles.buttonText}>Bottom Sheet</Text>
</TouchableOpacity>

<TouchableOpacity
style={[styles.button, styles.roktButtonAlt]}
onPress={handleRoktShoppableAds}
>
<Text style={styles.buttonText}>Shoppable Ads</Text>
</TouchableOpacity>
</View>

{/* Rokt Embedded Placeholder */}
Expand Down
83 changes: 70 additions & 13 deletions ExpoTestApp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,68 @@ The app also includes Rokt placement testing via the mParticle Rokt kit:
- **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.
- **Overlay**: Loads a full-screen overlay Rokt placement that appears on top of the app content.
- **Bottom Sheet**: Loads a bottom sheet Rokt placement that slides up from the bottom of the screen.
- **Shoppable Ads**: Calls `MParticle.Rokt.selectShoppableAds` with a staging placement identifier and checkout-style attributes (see implementation guide below).

The Rokt section also demonstrates:

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

### Implementation guide: Shoppable Ads (`selectShoppableAds`) and iOS payment extensions

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.

#### JavaScript: `selectShoppableAds`

Use `MParticle.Rokt.selectShoppableAds(identifier, attributes, roktConfig?)` when you need the Shoppable Ads experience instead of `selectPlacements`.

- **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).
- **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.
- **roktConfig**: Optional; the demo uses `MParticle.Rokt.createRoktConfig('system')` for color mode. Add a cache config if you use caching elsewhere.

Example (same pattern as `ExpoTestApp/App.tsx`):

```javascript
const config = MParticle.Rokt.createRoktConfig('system');

MParticle.Rokt.selectShoppableAds('YOUR_PLACEMENT_ID', attributes, config)
.then(() => {
/* success */
})
.catch(error => {
/* handle */
});
```

Listen for `RoktCallback` and `RoktEvents` on `RoktEventManager` to observe load/unload and Shoppable Ads–related events emitted by the native bridge.

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

#### iOS native: `RoktStripePaymentExtension` (payment extensions)

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.

In `ios/MParticleExpoTest/AppDelegate.swift`, the test app:

1. Imports the Stripe payment extension module provided with the Rokt / kit stack: `import RoktStripePaymentExtension`.
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).
3. Registers it: `MParticle.sharedInstance().rokt.register(paymentExtension)`.

```swift
import RoktStripePaymentExtension

// After MParticle.sharedInstance().start(with: mParticleOptions):
if let paymentExtension = RoktStripePaymentExtension(applePayMerchantId: "merchant.your.id") {
MParticle.sharedInstance().rokt.register(paymentExtension)
}
```

**Important:**

- 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.
- Ensure the **mParticle Rokt kit** (and transitive Rokt dependencies) are installed so `RoktStripePaymentExtension` resolves—same as configuring `iosKits`: `["mParticle-Rokt"]` in `app.json`.

All activity is logged in the Activity Log section at the bottom of the screen.

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

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.

#### Objective-C AppDelegate (Legacy)

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

## Plugin Configuration Options

| Option | Type | Description |
|--------|------|-------------|
| `iosApiKey` | string | mParticle iOS API key |
| `iosApiSecret` | string | mParticle iOS API secret |
| `androidApiKey` | string | mParticle Android API key |
| `androidApiSecret` | string | mParticle Android API secret |
| `logLevel` | string | Log level: `none`, `error`, `warning`, `debug`, `verbose` |
| `environment` | string | Environment: `development`, `production`, `autoDetect` |
| `useEmptyIdentifyRequest` | boolean | Initialize with empty identify request (default: true) |
| `dataPlanId` | string | Data plan ID for validation |
| `dataPlanVersion` | number | Data plan version |
| `iosKits` | string[] | iOS kit pod names (e.g., `["mParticle-Rokt"]`) |
| `androidKits` | string[] | Android kit dependencies (e.g., `["android-rokt-kit"]`) |
| Option | Type | Description |
| ------------------------- | -------- | --------------------------------------------------------- |
| `iosApiKey` | string | mParticle iOS API key |
| `iosApiSecret` | string | mParticle iOS API secret |
| `androidApiKey` | string | mParticle Android API key |
| `androidApiSecret` | string | mParticle Android API secret |
| `logLevel` | string | Log level: `none`, `error`, `warning`, `debug`, `verbose` |
| `environment` | string | Environment: `development`, `production`, `autoDetect` |
| `useEmptyIdentifyRequest` | boolean | Initialize with empty identify request (default: true) |
| `dataPlanId` | string | Data plan ID for validation |
| `dataPlanVersion` | number | Data plan version |
| `iosKits` | string[] | iOS kit pod names (e.g., `["mParticle-Rokt"]`) |
| `androidKits` | string[] | Android kit dependencies (e.g., `["android-rokt-kit"]`) |
5 changes: 3 additions & 2 deletions ExpoTestApp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
"prebuild": "expo prebuild --clean"
},
"dependencies": {
"react-native-mparticle": "file:../react-native-mparticle-latest.tgz",
"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"
"react-native": "0.81.5",
"react-native-mparticle": "file:.."
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.mparticle.MpRoktEventCallback
import com.mparticle.RoktEvent
import com.mparticle.UnloadReasons
import com.mparticle.WrapperSdk
import com.mparticle.internal.Logger
import com.mparticle.rokt.CacheConfig
import com.mparticle.rokt.RoktConfig
import kotlinx.coroutines.Job
Expand All @@ -39,6 +40,14 @@ class MPRoktModuleImpl(

fun getName(): String = MODULE_NAME

fun selectShoppableAds(
identifier: String,
attributes: ReadableMap?,
roktConfig: ReadableMap?,
) {
Logger.warning("selectShoppableAds is not yet supported on Android")
}

fun purchaseFinalized(
placementId: String,
catalogItemId: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,15 @@ import com.facebook.react.bridge.ReadableType
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.uimanager.UIManagerHelper
import com.mparticle.MParticle
import com.mparticle.WrapperSdk
import com.mparticle.internal.Logger
import com.mparticle.react.NativeMPRoktSpec
import com.mparticle.rokt.RoktEmbeddedView
import com.mparticle.internal.Logger
import java.lang.ref.WeakReference
import java.util.concurrent.CountDownLatch

class MPRoktModule(
private val reactContext: ReactApplicationContext,
) : NativeMPRoktSpec(reactContext) {

private val impl = MPRoktModuleImpl(reactContext)

override fun getName(): String = impl.getName()
Expand Down Expand Up @@ -52,6 +50,15 @@ class MPRoktModule(
)
}

@ReactMethod
override fun selectShoppableAds(
identifier: String,
attributes: ReadableMap?,
roktConfig: ReadableMap?,
) {
impl.selectShoppableAds(identifier, attributes, roktConfig)
}

@ReactMethod
override fun purchaseFinalized(
placementId: String,
Expand All @@ -61,7 +68,6 @@ class MPRoktModule(
impl.purchaseFinalized(placementId, catalogItemId, success)
}


/**
* Process placeholders from ReadableMap to a map of Widgets for use with Rokt.
* This method handles the Fabric-specific view resolution.
Expand All @@ -83,8 +89,9 @@ class MPRoktModule(
// Get the tag value as an integer
val reactTag =
when {
placeholders.getType(key) == ReadableType.Number ->
placeholders.getType(key) == ReadableType.Number -> {
placeholders.getDouble(key).toInt()
}

else -> {
Logger.warning("Invalid view tag for key: $key")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ abstract class NativeMPRoktSpec(
fontFilesMap: ReadableMap?,
)

abstract fun selectShoppableAds(
identifier: String,
attributes: ReadableMap?,
roktConfig: ReadableMap?,
)

abstract fun purchaseFinalized(
placementId: String,
catalogItemId: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ class MPRoktModule(
}
}

@ReactMethod
override fun selectShoppableAds(
identifier: String,
attributes: ReadableMap?,
roktConfig: ReadableMap?,
) {
impl.selectShoppableAds(identifier, attributes, roktConfig)
}

@ReactMethod
override fun purchaseFinalized(
placementId: String,
Expand Down
Loading
Loading