rampkit-expo-dev 0.0.51 → 0.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/expo/modules/rampkit/RampKitModule.kt +104 -33
- package/build/RampKit.d.ts +3 -2
- package/build/RampKit.js +26 -2
- package/build/RampkitOverlay.d.ts +4 -1
- package/build/RampkitOverlay.js +427 -29
- package/build/index.d.ts +1 -1
- package/build/types.d.ts +21 -0
- package/ios/RampKitModule.swift +93 -40
- package/package.json +1 -1
|
@@ -42,6 +42,7 @@ class RampKitModule : Module(), PurchasesUpdatedListener {
|
|
|
42
42
|
private val INSTALL_DATE_KEY = "rk_install_date"
|
|
43
43
|
private val LAUNCH_COUNT_KEY = "rk_launch_count"
|
|
44
44
|
private val LAST_LAUNCH_KEY = "rk_last_launch"
|
|
45
|
+
private val ORIGINAL_TXN_PREFIX = "rk_orig_txn_"
|
|
45
46
|
|
|
46
47
|
private var billingClient: BillingClient? = null
|
|
47
48
|
private var appId: String? = null
|
|
@@ -247,7 +248,7 @@ class RampKitModule : Module(), PurchasesUpdatedListener {
|
|
|
247
248
|
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
|
|
248
249
|
.setPurchaseToken(purchase.purchaseToken)
|
|
249
250
|
.build()
|
|
250
|
-
|
|
251
|
+
|
|
251
252
|
billingClient?.acknowledgePurchase(acknowledgePurchaseParams) { result ->
|
|
252
253
|
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
|
|
253
254
|
Log.d(TAG, "Purchase acknowledged")
|
|
@@ -257,53 +258,93 @@ class RampKitModule : Module(), PurchasesUpdatedListener {
|
|
|
257
258
|
|
|
258
259
|
// Get product details for price info
|
|
259
260
|
coroutineScope.launch {
|
|
260
|
-
val
|
|
261
|
-
|
|
261
|
+
val productId = purchase.products.firstOrNull() ?: ""
|
|
262
|
+
val productDetails = getProductDetails(productId)
|
|
263
|
+
|
|
264
|
+
// Determine originalTransactionId for subscription tracking
|
|
265
|
+
// For Android, we store the first orderId for each product as the "original"
|
|
266
|
+
val originalTransactionId = getOrStoreOriginalTransactionId(productId, purchase.orderId)
|
|
267
|
+
val isRenewal = originalTransactionId != purchase.orderId
|
|
268
|
+
|
|
269
|
+
// Determine event type (matching iOS SDK logic)
|
|
270
|
+
val eventName: String
|
|
271
|
+
val isTrial: Boolean
|
|
272
|
+
val isIntroOffer: Boolean
|
|
273
|
+
|
|
274
|
+
// Check for free trial or intro offer
|
|
275
|
+
val subscriptionOffer = productDetails?.subscriptionOfferDetails?.firstOrNull()
|
|
276
|
+
val firstPricingPhase = subscriptionOffer?.pricingPhases?.pricingPhaseList?.firstOrNull()
|
|
277
|
+
val hasFreeTrial = firstPricingPhase?.priceAmountMicros == 0L
|
|
278
|
+
|
|
279
|
+
when {
|
|
280
|
+
// Subscription renewal
|
|
281
|
+
isRenewal && purchase.isAutoRenewing -> {
|
|
282
|
+
eventName = "subscription_renewed"
|
|
283
|
+
isTrial = false
|
|
284
|
+
isIntroOffer = false
|
|
285
|
+
}
|
|
286
|
+
// Trial started (free trial)
|
|
287
|
+
hasFreeTrial && !isRenewal -> {
|
|
288
|
+
eventName = "trial_started"
|
|
289
|
+
isTrial = true
|
|
290
|
+
isIntroOffer = true
|
|
291
|
+
}
|
|
292
|
+
// Regular purchase
|
|
293
|
+
else -> {
|
|
294
|
+
eventName = "purchase_completed"
|
|
295
|
+
isTrial = false
|
|
296
|
+
isIntroOffer = false
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
262
300
|
val properties = mutableMapOf<String, Any?>(
|
|
263
|
-
"productId" to
|
|
301
|
+
"productId" to productId,
|
|
264
302
|
"transactionId" to purchase.orderId,
|
|
265
|
-
"originalTransactionId" to
|
|
303
|
+
"originalTransactionId" to originalTransactionId,
|
|
266
304
|
"purchaseToken" to purchase.purchaseToken,
|
|
267
305
|
"purchaseDate" to getIso8601Timestamp(purchase.purchaseTime),
|
|
268
306
|
"quantity" to purchase.quantity,
|
|
269
|
-
"isAutoRenewing" to purchase.isAutoRenewing
|
|
307
|
+
"isAutoRenewing" to purchase.isAutoRenewing,
|
|
308
|
+
"isTrial" to isTrial,
|
|
309
|
+
"isIntroOffer" to isIntroOffer
|
|
270
310
|
)
|
|
271
311
|
|
|
272
312
|
productDetails?.let { details ->
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
313
|
+
properties["productType"] = if (details.productType == BillingClient.ProductType.SUBS) "auto_renewable" else "non_consumable"
|
|
314
|
+
|
|
315
|
+
// Get pricing info
|
|
316
|
+
val oneTimePurchase = details.oneTimePurchaseOfferDetails
|
|
317
|
+
val subscriptionOfferDetails = details.subscriptionOfferDetails?.firstOrNull()
|
|
318
|
+
|
|
319
|
+
if (oneTimePurchase != null) {
|
|
320
|
+
properties["amount"] = oneTimePurchase.priceAmountMicros / 1_000_000.0
|
|
321
|
+
properties["currency"] = oneTimePurchase.priceCurrencyCode
|
|
322
|
+
properties["priceFormatted"] = oneTimePurchase.formattedPrice
|
|
323
|
+
} else if (subscriptionOfferDetails != null) {
|
|
324
|
+
// For subscriptions, get the base pricing phase (not the free trial phase)
|
|
325
|
+
val basePricingPhase = subscriptionOfferDetails.pricingPhases.pricingPhaseList
|
|
326
|
+
.firstOrNull { it.priceAmountMicros > 0 }
|
|
327
|
+
?: subscriptionOfferDetails.pricingPhases.pricingPhaseList.lastOrNull()
|
|
328
|
+
|
|
329
|
+
basePricingPhase?.let { phase ->
|
|
330
|
+
properties["amount"] = phase.priceAmountMicros / 1_000_000.0
|
|
331
|
+
properties["currency"] = phase.priceCurrencyCode
|
|
332
|
+
properties["priceFormatted"] = phase.formattedPrice
|
|
333
|
+
properties["subscriptionPeriod"] = formatBillingPeriod(phase.billingPeriod)
|
|
280
334
|
}
|
|
281
335
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
properties["currency"] = when (pricingPhase) {
|
|
290
|
-
is ProductDetails.PricingPhase -> pricingPhase.priceCurrencyCode
|
|
291
|
-
else -> (pricingPhase as? Any)?.let {
|
|
292
|
-
it.javaClass.getMethod("getPriceCurrencyCode").invoke(it) as String
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
properties["localizedPrice"] = when (pricingPhase) {
|
|
296
|
-
is ProductDetails.PricingPhase -> pricingPhase.formattedPrice
|
|
297
|
-
else -> (pricingPhase as? Any)?.let {
|
|
298
|
-
it.javaClass.getMethod("getFormattedPrice").invoke(it) as String
|
|
299
|
-
}
|
|
336
|
+
// Offer type detection
|
|
337
|
+
val freeTrialPhase = subscriptionOfferDetails.pricingPhases.pricingPhaseList
|
|
338
|
+
.firstOrNull { it.priceAmountMicros == 0L }
|
|
339
|
+
if (freeTrialPhase != null) {
|
|
340
|
+
properties["offerType"] = "introductory"
|
|
300
341
|
}
|
|
301
342
|
}
|
|
343
|
+
|
|
302
344
|
properties["localizedName"] = details.name
|
|
303
|
-
properties["productType"] = details.productType
|
|
304
345
|
}
|
|
305
346
|
|
|
306
|
-
sendPurchaseEvent(appId, userId,
|
|
347
|
+
sendPurchaseEvent(appId, userId, eventName, properties)
|
|
307
348
|
}
|
|
308
349
|
}
|
|
309
350
|
Purchase.PurchaseState.PENDING -> {
|
|
@@ -312,6 +353,36 @@ class RampKitModule : Module(), PurchasesUpdatedListener {
|
|
|
312
353
|
}
|
|
313
354
|
}
|
|
314
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Get or store the original transaction ID for a product.
|
|
358
|
+
* This enables subscription renewal tracking by comparing subsequent orderIds
|
|
359
|
+
* to the original orderId for the same product.
|
|
360
|
+
*/
|
|
361
|
+
private fun getOrStoreOriginalTransactionId(productId: String, currentOrderId: String?): String? {
|
|
362
|
+
if (currentOrderId == null) return null
|
|
363
|
+
|
|
364
|
+
val key = ORIGINAL_TXN_PREFIX + productId
|
|
365
|
+
val storedOriginal = prefs.getString(key, null)
|
|
366
|
+
|
|
367
|
+
return if (storedOriginal != null) {
|
|
368
|
+
// Return the stored original
|
|
369
|
+
storedOriginal
|
|
370
|
+
} else {
|
|
371
|
+
// This is the first purchase for this product, store it as original
|
|
372
|
+
prefs.edit().putString(key, currentOrderId).apply()
|
|
373
|
+
currentOrderId
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Format Google Play billing period to ISO 8601 duration format
|
|
379
|
+
* Input format: P1W, P1M, P3M, P6M, P1Y
|
|
380
|
+
*/
|
|
381
|
+
private fun formatBillingPeriod(billingPeriod: String): String {
|
|
382
|
+
// Google Play already uses ISO 8601 format, just return it
|
|
383
|
+
return billingPeriod
|
|
384
|
+
}
|
|
385
|
+
|
|
315
386
|
private suspend fun getProductDetails(productId: String): ProductDetails? {
|
|
316
387
|
if (productId.isEmpty()) return null
|
|
317
388
|
val client = billingClient ?: return null
|
package/build/RampKit.d.ts
CHANGED
|
@@ -68,7 +68,8 @@ export declare class RampKitCore {
|
|
|
68
68
|
*/
|
|
69
69
|
trackCtaTap(buttonId: string, buttonText?: string): void;
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
71
|
+
* Reset the SDK state and re-initialize
|
|
72
|
+
* Call this when a user logs out or when you need to clear all cached state
|
|
72
73
|
*/
|
|
73
|
-
|
|
74
|
+
reset(): Promise<void>;
|
|
74
75
|
}
|
package/build/RampKit.js
CHANGED
|
@@ -209,6 +209,13 @@ class RampKitCore {
|
|
|
209
209
|
const rampkitContext = this.deviceInfo
|
|
210
210
|
? (0, DeviceInfoCollector_1.buildRampKitContext)(this.deviceInfo)
|
|
211
211
|
: undefined;
|
|
212
|
+
// Extract navigation data for spatial layout-based navigation
|
|
213
|
+
const navigation = data.navigation
|
|
214
|
+
? {
|
|
215
|
+
mainFlow: data.navigation.mainFlow || [],
|
|
216
|
+
screenPositions: data.navigation.screenPositions,
|
|
217
|
+
}
|
|
218
|
+
: undefined;
|
|
212
219
|
// Track onboarding started event
|
|
213
220
|
const onboardingId = data.onboardingId || data.id || "unknown";
|
|
214
221
|
EventManager_1.eventManager.trackOnboardingStarted(onboardingId, screens.length);
|
|
@@ -229,6 +236,7 @@ class RampKitCore {
|
|
|
229
236
|
variables,
|
|
230
237
|
requiredScripts,
|
|
231
238
|
rampkitContext,
|
|
239
|
+
navigation,
|
|
232
240
|
onOnboardingFinished: (payload) => {
|
|
233
241
|
var _a;
|
|
234
242
|
// Track onboarding completed
|
|
@@ -290,18 +298,34 @@ class RampKitCore {
|
|
|
290
298
|
// StoreKit 2 (iOS) and Google Play Billing (Android) transaction observers.
|
|
291
299
|
// No manual tracking is needed.
|
|
292
300
|
/**
|
|
293
|
-
*
|
|
301
|
+
* Reset the SDK state and re-initialize
|
|
302
|
+
* Call this when a user logs out or when you need to clear all cached state
|
|
294
303
|
*/
|
|
295
|
-
async
|
|
304
|
+
async reset() {
|
|
305
|
+
if (!this.config) {
|
|
306
|
+
console.warn("[RampKit] Reset: No config found, cannot re-initialize");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
console.log("[RampKit] Reset: Clearing SDK state...");
|
|
296
310
|
// Stop transaction observer
|
|
297
311
|
await RampKitNative_1.TransactionObserver.stop();
|
|
312
|
+
// Remove app state listener
|
|
298
313
|
if (this.appStateSubscription) {
|
|
299
314
|
this.appStateSubscription.remove();
|
|
300
315
|
this.appStateSubscription = null;
|
|
301
316
|
}
|
|
317
|
+
// Reset event manager state
|
|
302
318
|
EventManager_1.eventManager.reset();
|
|
319
|
+
// Reset session
|
|
303
320
|
(0, DeviceInfoCollector_1.resetSession)();
|
|
321
|
+
// Clear local state
|
|
322
|
+
this.userId = null;
|
|
323
|
+
this.deviceInfo = null;
|
|
324
|
+
this.onboardingData = null;
|
|
304
325
|
this.initialized = false;
|
|
326
|
+
console.log("[RampKit] Reset: Re-initializing SDK...");
|
|
327
|
+
// Re-initialize with stored config
|
|
328
|
+
await this.init(this.config);
|
|
305
329
|
}
|
|
306
330
|
}
|
|
307
331
|
exports.RampKitCore = RampKitCore;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { RampKitContext } from "./types";
|
|
1
|
+
import { RampKitContext, NavigationData } from "./types";
|
|
2
2
|
export declare const injectedHardening = "\n(function(){\n try {\n var meta = document.querySelector('meta[name=\"viewport\"]');\n if (!meta) { meta = document.createElement('meta'); meta.name = 'viewport'; document.head.appendChild(meta); }\n meta.setAttribute('content','width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover');\n var style = document.createElement('style');\n style.textContent='html,body{overflow-x:hidden!important;} html,body,*{-webkit-user-select:none!important;user-select:none!important;-webkit-touch-callout:none!important;-ms-user-select:none!important;touch-action: pan-y;} *{-webkit-tap-highlight-color: rgba(0,0,0,0)!important;} ::selection{background: transparent!important;} ::-moz-selection{background: transparent!important;} a,img{-webkit-user-drag:none!important;user-drag:none!important;-webkit-touch-callout:none!important} input,textarea{caret-color:transparent!important;-webkit-user-select:none!important;user-select:none!important}';\n document.head.appendChild(style);\n var prevent=function(e){e.preventDefault&&e.preventDefault();};\n document.addEventListener('gesturestart',prevent,{passive:false});\n document.addEventListener('gesturechange',prevent,{passive:false});\n document.addEventListener('gestureend',prevent,{passive:false});\n document.addEventListener('dblclick',prevent,{passive:false});\n document.addEventListener('wheel',function(e){ if(e.ctrlKey) e.preventDefault(); },{passive:false});\n document.addEventListener('touchmove',function(e){ if(e.scale && e.scale !== 1) e.preventDefault(); },{passive:false});\n document.addEventListener('selectstart',prevent,{passive:false,capture:true});\n document.addEventListener('contextmenu',prevent,{passive:false,capture:true});\n document.addEventListener('copy',prevent,{passive:false,capture:true});\n document.addEventListener('cut',prevent,{passive:false,capture:true});\n document.addEventListener('paste',prevent,{passive:false,capture:true});\n document.addEventListener('dragstart',prevent,{passive:false,capture:true});\n // Belt-and-suspenders: aggressively clear any attempted selection\n var clearSel=function(){\n try{var sel=window.getSelection&&window.getSelection(); if(sel&&sel.removeAllRanges) sel.removeAllRanges();}catch(_){} }\n document.addEventListener('selectionchange',clearSel,{passive:true,capture:true});\n document.onselectstart=function(){ clearSel(); return false; };\n try{ document.documentElement.style.webkitUserSelect='none'; document.documentElement.style.userSelect='none'; }catch(_){ }\n try{ document.body.style.webkitUserSelect='none'; document.body.style.userSelect='none'; }catch(_){ }\n var __selTimer = setInterval(clearSel, 160);\n window.addEventListener('pagehide',function(){ try{ clearInterval(__selTimer); }catch(_){} });\n // Continuously enforce no-select on all elements and new nodes\n var enforceNoSelect = function(el){\n try{\n el.style && (el.style.webkitUserSelect='none', el.style.userSelect='none', el.style.webkitTouchCallout='none');\n el.setAttribute && (el.setAttribute('unselectable','on'), el.setAttribute('contenteditable','false'));\n }catch(_){}\n }\n try{\n var all=document.getElementsByTagName('*');\n for(var i=0;i<all.length;i++){ enforceNoSelect(all[i]); }\n var obs = new MutationObserver(function(muts){\n for(var j=0;j<muts.length;j++){\n var m=muts[j];\n if(m.type==='childList'){\n m.addedNodes && m.addedNodes.forEach && m.addedNodes.forEach(function(n){ if(n && n.nodeType===1){ enforceNoSelect(n); var q=n.getElementsByTagName? n.getElementsByTagName('*'): []; for(var k=0;k<q.length;k++){ enforceNoSelect(q[k]); }}});\n } else if(m.type==='attributes'){\n enforceNoSelect(m.target);\n }\n }\n });\n obs.observe(document.documentElement,{ childList:true, subtree:true, attributes:true, attributeFilter:['contenteditable','style'] });\n }catch(_){ }\n } catch(_) {}\n})(); true;\n";
|
|
3
3
|
export declare const injectedNoSelect = "\n(function(){\n try {\n if (window.__rkNoSelectApplied) return true;\n window.__rkNoSelectApplied = true;\n var style = document.getElementById('rk-no-select-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'rk-no-select-style';\n style.innerHTML = \"\n * {\n user-select: none !important;\n -webkit-user-select: none !important;\n -webkit-touch-callout: none !important;\n }\n ::selection {\n background: transparent !important;\n }\n \";\n document.head.appendChild(style);\n }\n var prevent = function(e){ if(e && e.preventDefault) e.preventDefault(); return false; };\n document.addEventListener('contextmenu', prevent, { passive: false, capture: true });\n document.addEventListener('selectstart', prevent, { passive: false, capture: true });\n } catch (_) {}\n true;\n})();\n";
|
|
4
4
|
export declare const injectedVarsHandler = "\n(function(){\n try {\n if (window.__rkVarsHandlerApplied) return true;\n window.__rkVarsHandlerApplied = true;\n \n // Handler function that updates variables and notifies the page\n window.__rkHandleVarsUpdate = function(vars) {\n if (!vars || typeof vars !== 'object') return;\n // Update the global variables object\n window.__rampkitVariables = vars;\n // Dispatch a custom event that the page's JS can listen to for re-rendering\n try {\n document.dispatchEvent(new CustomEvent('rampkit:vars-updated', { detail: vars }));\n } catch(e) {}\n // Also try calling a global handler if the page defined one\n try {\n if (typeof window.onRampkitVarsUpdate === 'function') {\n window.onRampkitVarsUpdate(vars);\n }\n } catch(e) {}\n };\n \n // Listen for message events from React Native\n document.addEventListener('message', function(event) {\n try {\n var data = event.data;\n if (data && data.type === 'rampkit:variables' && data.vars) {\n window.__rkHandleVarsUpdate(data.vars);\n }\n } catch(e) {}\n }, false);\n \n // Also listen on window for compatibility\n window.addEventListener('message', function(event) {\n try {\n var data = event.data;\n if (data && data.type === 'rampkit:variables' && data.vars) {\n window.__rkHandleVarsUpdate(data.vars);\n }\n } catch(e) {}\n }, false);\n } catch (_) {}\n true;\n})();\n";
|
|
5
|
+
export declare const injectedDynamicTapHandler = "\n(function() {\n if (window.__rampkitClickInterceptorInstalled) return;\n window.__rampkitClickInterceptorInstalled = true;\n \n // Decode HTML entities\n function decodeHtml(str) {\n if (!str) return str;\n return str.replace(/"/g, '\"').replace(/"/g, '\"').replace(/"/g, '\"')\n .replace(/'/g, \"'\").replace(/'/g, \"'\").replace(/'/g, \"'\")\n .replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');\n }\n \n // Find dynamic tap config on element or ancestors\n function findDynamicTap(el) {\n var current = el;\n var depth = 0;\n var attrNames = ['data-tap-dynamic', 'data-tapdynamic', 'tapDynamic', 'data-dynamic-tap'];\n while (current && current !== document.body && current !== document.documentElement && depth < 20) {\n if (current.getAttribute) {\n for (var i = 0; i < attrNames.length; i++) {\n var attr = current.getAttribute(attrNames[i]);\n if (attr && attr.length > 2) {\n return { element: current, config: attr };\n }\n }\n if (current.dataset && current.dataset.tapDynamic) {\n return { element: current, config: current.dataset.tapDynamic };\n }\n }\n current = current.parentElement;\n depth++;\n }\n return null;\n }\n \n // Get variables for condition evaluation - check ALL possible sources\n function getVars() {\n var vars = {};\n if (window.__rampkitVariables) {\n Object.keys(window.__rampkitVariables).forEach(function(k) {\n vars[k] = window.__rampkitVariables[k];\n });\n }\n if (window.__rampkitVars) {\n Object.keys(window.__rampkitVars).forEach(function(k) {\n vars[k] = window.__rampkitVars[k];\n });\n }\n if (window.RK_VARS) {\n Object.keys(window.RK_VARS).forEach(function(k) {\n vars[k] = window.RK_VARS[k];\n });\n }\n return vars;\n }\n \n // Evaluate a single rule\n function evalRule(rule, vars) {\n if (!rule || !rule.key) return false;\n var left = vars[rule.key];\n var right = rule.value;\n var op = rule.op || '=';\n if (left === undefined || left === null) left = '';\n if (right === undefined || right === null) right = '';\n var leftStr = String(left);\n var rightStr = String(right);\n var result = false;\n switch (op) {\n case '=': case '==': result = leftStr === rightStr; break;\n case '!=': case '<>': result = leftStr !== rightStr; break;\n case '>': result = parseFloat(left) > parseFloat(right); break;\n case '<': result = parseFloat(left) < parseFloat(right); break;\n case '>=': result = parseFloat(left) >= parseFloat(right); break;\n case '<=': result = parseFloat(left) <= parseFloat(right); break;\n default: result = false;\n }\n return result;\n }\n \n // Evaluate all rules (AND logic)\n function evalRules(rules, vars) {\n if (!rules || !rules.length) return true;\n for (var i = 0; i < rules.length; i++) {\n if (!evalRule(rules[i], vars)) return false;\n }\n return true;\n }\n \n // Execute an action\n function execAction(action) {\n if (!action || !action.type) return;\n var msg = null;\n var actionType = action.type.toLowerCase();\n \n switch (actionType) {\n case 'navigate':\n msg = { type: 'rampkit:navigate', targetScreenId: action.targetScreenId || '__continue__', animation: action.animation || 'fade' };\n break;\n case 'continue':\n msg = { type: 'rampkit:navigate', targetScreenId: '__continue__', animation: action.animation || 'fade' };\n break;\n case 'goback':\n msg = { type: 'rampkit:goBack', animation: action.animation || 'fade' };\n break;\n case 'close':\n msg = { type: 'rampkit:close' };\n break;\n case 'haptic':\n msg = { type: 'rampkit:haptic', hapticType: action.hapticType || 'impact', impactStyle: action.impactStyle || 'Medium', notificationType: action.notificationType };\n break;\n case 'showpaywall':\n msg = { type: 'rampkit:show-paywall', payload: action.payload || { paywallId: action.paywallId } };\n break;\n case 'requestreview':\n msg = { type: 'rampkit:request-review' };\n break;\n case 'requestnotificationpermission':\n msg = { type: 'rampkit:request-notification-permission' };\n break;\n case 'onboardingfinished':\n msg = { type: 'rampkit:onboarding-finished', payload: action.payload };\n break;\n case 'setvariable':\n case 'setstate':\n case 'updatevariable':\n case 'set':\n case 'assign':\n var varKey = action.key || action.variableName || action.name || action.variable;\n var varValue = action.variableValue !== undefined ? action.variableValue :\n action.value !== undefined ? action.value :\n action.newValue !== undefined ? action.newValue : undefined;\n if (varKey && varValue !== undefined) {\n if (window.__rampkitVariables) window.__rampkitVariables[varKey] = varValue;\n if (window.__rampkitVars) window.__rampkitVars[varKey] = varValue;\n var updateVars = {};\n updateVars[varKey] = varValue;\n msg = { type: 'rampkit:variables', vars: updateVars };\n }\n break;\n }\n if (msg) {\n try {\n if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {\n window.ReactNativeWebView.postMessage(JSON.stringify(msg));\n }\n } catch(e) {}\n }\n }\n \n // Evaluate dynamic tap config\n function evalDynamicTap(config) {\n if (!config || !config.values) return false;\n var vars = getVars();\n var conditions = config.values;\n for (var i = 0; i < conditions.length; i++) {\n var cond = conditions[i];\n var condType = cond.conditionType || 'if';\n var rules = cond.rules || [];\n var actions = cond.actions || [];\n if (condType === 'else' || evalRules(rules, vars)) {\n for (var j = 0; j < actions.length; j++) {\n execAction(actions[j]);\n }\n return true;\n }\n }\n return false;\n }\n \n // Click interceptor - capture phase, runs BEFORE onclick handlers\n function interceptClick(event) {\n var result = findDynamicTap(event.target);\n if (!result) return;\n \n try {\n var configStr = decodeHtml(result.config);\n var config = JSON.parse(configStr);\n var handled = evalDynamicTap(config);\n if (handled) {\n event.stopImmediatePropagation();\n event.preventDefault();\n return false;\n }\n } catch (e) {\n console.log('[RampKit] Dynamic tap error:', e);\n }\n }\n \n // Install interceptor on window in capture phase\n window.addEventListener('click', interceptClick, true);\n})();\n";
|
|
5
6
|
export declare const injectedButtonAnimations = "\n(function(){\n try {\n if (window.__rkButtonAnimApplied) return true;\n window.__rkButtonAnimApplied = true;\n \n var pressed = null;\n var pressedOriginalTransform = '';\n var pressedOriginalOpacity = '';\n var pressedOriginalTransition = '';\n var releaseTimer = null;\n \n // Find interactive element - very permissive, looks for any clickable-looking element\n function findInteractive(el) {\n var current = el;\n for (var i = 0; i < 20 && current && current !== document.body && current !== document.documentElement; i++) {\n if (!current || !current.tagName) { current = current.parentElement; continue; }\n var tag = current.tagName.toLowerCase();\n \n // Skip tiny elements (likely icons inside buttons)\n var rect = current.getBoundingClientRect();\n if (rect.width < 20 || rect.height < 20) { current = current.parentElement; continue; }\n \n // Match standard interactive elements\n if (tag === 'button' || tag === 'a' || tag === 'input' || tag === 'select') return current;\n \n // Match elements with any data attribute containing action/navigate/tap/click\n var attrs = current.attributes;\n if (attrs) {\n for (var j = 0; j < attrs.length; j++) {\n var attrName = attrs[j].name.toLowerCase();\n if (attrName.indexOf('click') !== -1 || attrName.indexOf('tap') !== -1 || \n attrName.indexOf('action') !== -1 || attrName.indexOf('navigate') !== -1 ||\n attrName.indexOf('press') !== -1) {\n return current;\n }\n }\n }\n \n // Match elements with onclick\n if (current.onclick || current.hasAttribute('onclick')) return current;\n \n // Match elements with role=\"button\" or tabindex\n if (current.getAttribute('role') === 'button') return current;\n \n // Match any element with an ID containing button/btn/cta\n var id = current.id || '';\n if (id && (id.toLowerCase().indexOf('button') !== -1 || id.toLowerCase().indexOf('btn') !== -1 || id.toLowerCase().indexOf('cta') !== -1)) return current;\n \n // Match elements with button-like classes\n var className = current.className;\n if (className && typeof className === 'string') {\n var cls = className.toLowerCase();\n if (cls.indexOf('btn') !== -1 || cls.indexOf('button') !== -1 || cls.indexOf('cta') !== -1 || \n cls.indexOf('clickable') !== -1 || cls.indexOf('tappable') !== -1 || cls.indexOf('pressable') !== -1) {\n return current;\n }\n }\n \n // Match elements with cursor pointer\n try {\n var computed = window.getComputedStyle(current);\n if (computed && computed.cursor === 'pointer') return current;\n } catch(e) {}\n \n current = current.parentElement;\n }\n return null;\n }\n \n function applyPressedStyle(el) {\n if (!el || !el.style) return;\n // Save original styles\n pressedOriginalTransform = el.style.transform || '';\n pressedOriginalOpacity = el.style.opacity || '';\n pressedOriginalTransition = el.style.transition || '';\n // Apply pressed style with inline styles for maximum specificity\n el.style.transition = 'transform 80ms cubic-bezier(0.25, 0.1, 0.25, 1), opacity 80ms cubic-bezier(0.25, 0.1, 0.25, 1)';\n el.style.transform = 'scale(0.97)';\n el.style.opacity = '0.8';\n }\n \n function applyReleasedStyle(el) {\n if (!el || !el.style) return;\n // Apply spring-back animation\n el.style.transition = 'transform 280ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 280ms cubic-bezier(0.34, 1.56, 0.64, 1)';\n el.style.transform = pressedOriginalTransform || 'scale(1)';\n el.style.opacity = pressedOriginalOpacity || '1';\n }\n \n function resetStyle(el) {\n if (!el || !el.style) return;\n el.style.transform = pressedOriginalTransform;\n el.style.opacity = pressedOriginalOpacity;\n el.style.transition = pressedOriginalTransition;\n }\n \n function onTouchStart(e) {\n try {\n var target = findInteractive(e.target);\n if (!target) return;\n if (releaseTimer) { clearTimeout(releaseTimer); releaseTimer = null; }\n if (pressed && pressed !== target) { resetStyle(pressed); }\n applyPressedStyle(target);\n pressed = target;\n } catch(err) {}\n }\n \n function onTouchEnd(e) {\n try {\n if (!pressed) return;\n var t = pressed;\n applyReleasedStyle(t);\n releaseTimer = setTimeout(function() {\n resetStyle(t);\n releaseTimer = null;\n }, 300);\n pressed = null;\n } catch(err) {}\n }\n \n function onTouchCancel(e) {\n try {\n if (!pressed) return;\n resetStyle(pressed);\n pressed = null;\n if (releaseTimer) { clearTimeout(releaseTimer); releaseTimer = null; }\n } catch(err) {}\n }\n \n // Use capture phase for immediate response before any other handlers\n document.addEventListener('touchstart', onTouchStart, { passive: true, capture: true });\n document.addEventListener('touchend', onTouchEnd, { passive: true, capture: true });\n document.addEventListener('touchcancel', onTouchCancel, { passive: true, capture: true });\n // Mouse events for testing\n document.addEventListener('mousedown', onTouchStart, { passive: true, capture: true });\n document.addEventListener('mouseup', onTouchEnd, { passive: true, capture: true });\n \n } catch (err) {}\n true;\n})();\n";
|
|
6
7
|
export type ScreenPayload = {
|
|
7
8
|
id: string;
|
|
@@ -15,6 +16,7 @@ export declare function showRampkitOverlay(opts: {
|
|
|
15
16
|
variables?: Record<string, any>;
|
|
16
17
|
requiredScripts?: string[];
|
|
17
18
|
rampkitContext?: RampKitContext;
|
|
19
|
+
navigation?: NavigationData;
|
|
18
20
|
onClose?: () => void;
|
|
19
21
|
onOnboardingFinished?: (payload?: any) => void;
|
|
20
22
|
onShowPaywall?: (payload?: any) => void;
|
|
@@ -38,6 +40,7 @@ declare function Overlay(props: {
|
|
|
38
40
|
variables?: Record<string, any>;
|
|
39
41
|
requiredScripts?: string[];
|
|
40
42
|
rampkitContext?: RampKitContext;
|
|
43
|
+
navigation?: NavigationData;
|
|
41
44
|
prebuiltDocs?: string[];
|
|
42
45
|
onRequestClose: () => void;
|
|
43
46
|
onOnboardingFinished?: (payload?: any) => void;
|
package/build/RampkitOverlay.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.injectedButtonAnimations = exports.injectedVarsHandler = exports.injectedNoSelect = exports.injectedHardening = void 0;
|
|
39
|
+
exports.injectedButtonAnimations = exports.injectedDynamicTapHandler = exports.injectedVarsHandler = exports.injectedNoSelect = exports.injectedHardening = void 0;
|
|
40
40
|
exports.showRampkitOverlay = showRampkitOverlay;
|
|
41
41
|
exports.hideRampkitOverlay = hideRampkitOverlay;
|
|
42
42
|
exports.closeRampkitOverlay = closeRampkitOverlay;
|
|
@@ -173,6 +173,201 @@ exports.injectedVarsHandler = `
|
|
|
173
173
|
true;
|
|
174
174
|
})();
|
|
175
175
|
`;
|
|
176
|
+
// Dynamic tap behavior handler - intercepts clicks and evaluates conditions
|
|
177
|
+
// Must run BEFORE content loads to capture all clicks
|
|
178
|
+
exports.injectedDynamicTapHandler = `
|
|
179
|
+
(function() {
|
|
180
|
+
if (window.__rampkitClickInterceptorInstalled) return;
|
|
181
|
+
window.__rampkitClickInterceptorInstalled = true;
|
|
182
|
+
|
|
183
|
+
// Decode HTML entities
|
|
184
|
+
function decodeHtml(str) {
|
|
185
|
+
if (!str) return str;
|
|
186
|
+
return str.replace(/"/g, '"').replace(/"/g, '"').replace(/"/g, '"')
|
|
187
|
+
.replace(/'/g, "'").replace(/'/g, "'").replace(/'/g, "'")
|
|
188
|
+
.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Find dynamic tap config on element or ancestors
|
|
192
|
+
function findDynamicTap(el) {
|
|
193
|
+
var current = el;
|
|
194
|
+
var depth = 0;
|
|
195
|
+
var attrNames = ['data-tap-dynamic', 'data-tapdynamic', 'tapDynamic', 'data-dynamic-tap'];
|
|
196
|
+
while (current && current !== document.body && current !== document.documentElement && depth < 20) {
|
|
197
|
+
if (current.getAttribute) {
|
|
198
|
+
for (var i = 0; i < attrNames.length; i++) {
|
|
199
|
+
var attr = current.getAttribute(attrNames[i]);
|
|
200
|
+
if (attr && attr.length > 2) {
|
|
201
|
+
return { element: current, config: attr };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (current.dataset && current.dataset.tapDynamic) {
|
|
205
|
+
return { element: current, config: current.dataset.tapDynamic };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
current = current.parentElement;
|
|
209
|
+
depth++;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Get variables for condition evaluation - check ALL possible sources
|
|
215
|
+
function getVars() {
|
|
216
|
+
var vars = {};
|
|
217
|
+
if (window.__rampkitVariables) {
|
|
218
|
+
Object.keys(window.__rampkitVariables).forEach(function(k) {
|
|
219
|
+
vars[k] = window.__rampkitVariables[k];
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (window.__rampkitVars) {
|
|
223
|
+
Object.keys(window.__rampkitVars).forEach(function(k) {
|
|
224
|
+
vars[k] = window.__rampkitVars[k];
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (window.RK_VARS) {
|
|
228
|
+
Object.keys(window.RK_VARS).forEach(function(k) {
|
|
229
|
+
vars[k] = window.RK_VARS[k];
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return vars;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Evaluate a single rule
|
|
236
|
+
function evalRule(rule, vars) {
|
|
237
|
+
if (!rule || !rule.key) return false;
|
|
238
|
+
var left = vars[rule.key];
|
|
239
|
+
var right = rule.value;
|
|
240
|
+
var op = rule.op || '=';
|
|
241
|
+
if (left === undefined || left === null) left = '';
|
|
242
|
+
if (right === undefined || right === null) right = '';
|
|
243
|
+
var leftStr = String(left);
|
|
244
|
+
var rightStr = String(right);
|
|
245
|
+
var result = false;
|
|
246
|
+
switch (op) {
|
|
247
|
+
case '=': case '==': result = leftStr === rightStr; break;
|
|
248
|
+
case '!=': case '<>': result = leftStr !== rightStr; break;
|
|
249
|
+
case '>': result = parseFloat(left) > parseFloat(right); break;
|
|
250
|
+
case '<': result = parseFloat(left) < parseFloat(right); break;
|
|
251
|
+
case '>=': result = parseFloat(left) >= parseFloat(right); break;
|
|
252
|
+
case '<=': result = parseFloat(left) <= parseFloat(right); break;
|
|
253
|
+
default: result = false;
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Evaluate all rules (AND logic)
|
|
259
|
+
function evalRules(rules, vars) {
|
|
260
|
+
if (!rules || !rules.length) return true;
|
|
261
|
+
for (var i = 0; i < rules.length; i++) {
|
|
262
|
+
if (!evalRule(rules[i], vars)) return false;
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Execute an action
|
|
268
|
+
function execAction(action) {
|
|
269
|
+
if (!action || !action.type) return;
|
|
270
|
+
var msg = null;
|
|
271
|
+
var actionType = action.type.toLowerCase();
|
|
272
|
+
|
|
273
|
+
switch (actionType) {
|
|
274
|
+
case 'navigate':
|
|
275
|
+
msg = { type: 'rampkit:navigate', targetScreenId: action.targetScreenId || '__continue__', animation: action.animation || 'fade' };
|
|
276
|
+
break;
|
|
277
|
+
case 'continue':
|
|
278
|
+
msg = { type: 'rampkit:navigate', targetScreenId: '__continue__', animation: action.animation || 'fade' };
|
|
279
|
+
break;
|
|
280
|
+
case 'goback':
|
|
281
|
+
msg = { type: 'rampkit:goBack', animation: action.animation || 'fade' };
|
|
282
|
+
break;
|
|
283
|
+
case 'close':
|
|
284
|
+
msg = { type: 'rampkit:close' };
|
|
285
|
+
break;
|
|
286
|
+
case 'haptic':
|
|
287
|
+
msg = { type: 'rampkit:haptic', hapticType: action.hapticType || 'impact', impactStyle: action.impactStyle || 'Medium', notificationType: action.notificationType };
|
|
288
|
+
break;
|
|
289
|
+
case 'showpaywall':
|
|
290
|
+
msg = { type: 'rampkit:show-paywall', payload: action.payload || { paywallId: action.paywallId } };
|
|
291
|
+
break;
|
|
292
|
+
case 'requestreview':
|
|
293
|
+
msg = { type: 'rampkit:request-review' };
|
|
294
|
+
break;
|
|
295
|
+
case 'requestnotificationpermission':
|
|
296
|
+
msg = { type: 'rampkit:request-notification-permission' };
|
|
297
|
+
break;
|
|
298
|
+
case 'onboardingfinished':
|
|
299
|
+
msg = { type: 'rampkit:onboarding-finished', payload: action.payload };
|
|
300
|
+
break;
|
|
301
|
+
case 'setvariable':
|
|
302
|
+
case 'setstate':
|
|
303
|
+
case 'updatevariable':
|
|
304
|
+
case 'set':
|
|
305
|
+
case 'assign':
|
|
306
|
+
var varKey = action.key || action.variableName || action.name || action.variable;
|
|
307
|
+
var varValue = action.variableValue !== undefined ? action.variableValue :
|
|
308
|
+
action.value !== undefined ? action.value :
|
|
309
|
+
action.newValue !== undefined ? action.newValue : undefined;
|
|
310
|
+
if (varKey && varValue !== undefined) {
|
|
311
|
+
if (window.__rampkitVariables) window.__rampkitVariables[varKey] = varValue;
|
|
312
|
+
if (window.__rampkitVars) window.__rampkitVars[varKey] = varValue;
|
|
313
|
+
var updateVars = {};
|
|
314
|
+
updateVars[varKey] = varValue;
|
|
315
|
+
msg = { type: 'rampkit:variables', vars: updateVars };
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
if (msg) {
|
|
320
|
+
try {
|
|
321
|
+
if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
|
|
322
|
+
window.ReactNativeWebView.postMessage(JSON.stringify(msg));
|
|
323
|
+
}
|
|
324
|
+
} catch(e) {}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Evaluate dynamic tap config
|
|
329
|
+
function evalDynamicTap(config) {
|
|
330
|
+
if (!config || !config.values) return false;
|
|
331
|
+
var vars = getVars();
|
|
332
|
+
var conditions = config.values;
|
|
333
|
+
for (var i = 0; i < conditions.length; i++) {
|
|
334
|
+
var cond = conditions[i];
|
|
335
|
+
var condType = cond.conditionType || 'if';
|
|
336
|
+
var rules = cond.rules || [];
|
|
337
|
+
var actions = cond.actions || [];
|
|
338
|
+
if (condType === 'else' || evalRules(rules, vars)) {
|
|
339
|
+
for (var j = 0; j < actions.length; j++) {
|
|
340
|
+
execAction(actions[j]);
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Click interceptor - capture phase, runs BEFORE onclick handlers
|
|
349
|
+
function interceptClick(event) {
|
|
350
|
+
var result = findDynamicTap(event.target);
|
|
351
|
+
if (!result) return;
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
var configStr = decodeHtml(result.config);
|
|
355
|
+
var config = JSON.parse(configStr);
|
|
356
|
+
var handled = evalDynamicTap(config);
|
|
357
|
+
if (handled) {
|
|
358
|
+
event.stopImmediatePropagation();
|
|
359
|
+
event.preventDefault();
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
} catch (e) {
|
|
363
|
+
console.log('[RampKit] Dynamic tap error:', e);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Install interceptor on window in capture phase
|
|
368
|
+
window.addEventListener('click', interceptClick, true);
|
|
369
|
+
})();
|
|
370
|
+
`;
|
|
176
371
|
// Button tap animation script - handles spring animations for interactive elements
|
|
177
372
|
// Triggers on touchstart (not click) for immediate feedback
|
|
178
373
|
// Uses inline styles for maximum compatibility
|
|
@@ -319,7 +514,7 @@ exports.injectedButtonAnimations = `
|
|
|
319
514
|
})();
|
|
320
515
|
`;
|
|
321
516
|
function performRampkitHaptic(event) {
|
|
322
|
-
if (!event
|
|
517
|
+
if (!event) {
|
|
323
518
|
// Backwards compatible default
|
|
324
519
|
try {
|
|
325
520
|
RampKitNative_1.Haptics.impactAsync("medium").catch(() => { });
|
|
@@ -327,7 +522,8 @@ function performRampkitHaptic(event) {
|
|
|
327
522
|
catch (_) { }
|
|
328
523
|
return;
|
|
329
524
|
}
|
|
330
|
-
|
|
525
|
+
// Accept messages with action: "haptic" OR just type: "rampkit:haptic"
|
|
526
|
+
const hapticType = event.hapticType || "impact";
|
|
331
527
|
try {
|
|
332
528
|
if (hapticType === "impact") {
|
|
333
529
|
const styleMap = {
|
|
@@ -376,7 +572,7 @@ function showRampkitOverlay(opts) {
|
|
|
376
572
|
return; // already visible
|
|
377
573
|
// Always build fresh docs to ensure templates are resolved with current context
|
|
378
574
|
const prebuiltDocs = undefined;
|
|
379
|
-
sibling = new react_native_root_siblings_1.default(((0, jsx_runtime_1.jsx)(Overlay, { onboardingId: opts.onboardingId, screens: opts.screens, variables: opts.variables, requiredScripts: opts.requiredScripts, rampkitContext: opts.rampkitContext, prebuiltDocs: prebuiltDocs, onRequestClose: () => {
|
|
575
|
+
sibling = new react_native_root_siblings_1.default(((0, jsx_runtime_1.jsx)(Overlay, { onboardingId: opts.onboardingId, screens: opts.screens, variables: opts.variables, requiredScripts: opts.requiredScripts, rampkitContext: opts.rampkitContext, navigation: opts.navigation, prebuiltDocs: prebuiltDocs, onRequestClose: () => {
|
|
380
576
|
var _a;
|
|
381
577
|
activeCloseHandler = null;
|
|
382
578
|
hideRampkitOverlay();
|
|
@@ -829,6 +1025,133 @@ function Overlay(props) {
|
|
|
829
1025
|
const lastVarsSendTimeRef = (0, react_1.useRef)([]);
|
|
830
1026
|
// Stale value window in milliseconds - matches iOS SDK (600ms)
|
|
831
1027
|
const STALE_VALUE_WINDOW_MS = 600;
|
|
1028
|
+
// ============================================================================
|
|
1029
|
+
// Navigation Resolution Helpers (matches iOS SDK behavior)
|
|
1030
|
+
// ============================================================================
|
|
1031
|
+
/**
|
|
1032
|
+
* Resolve `__continue__` to the actual target screen ID using navigation data
|
|
1033
|
+
* @param fromScreenId - The current screen ID
|
|
1034
|
+
* @returns The target screen ID, or null if at the end of the flow or should use fallback
|
|
1035
|
+
*/
|
|
1036
|
+
const resolveContinue = (fromScreenId) => {
|
|
1037
|
+
const navigation = props.navigation;
|
|
1038
|
+
// If no navigation data, fall back to array order
|
|
1039
|
+
if (!(navigation === null || navigation === void 0 ? void 0 : navigation.mainFlow) || navigation.mainFlow.length === 0) {
|
|
1040
|
+
console.log("[RampKit] 🧭 No navigation.mainFlow, using array order for __continue__");
|
|
1041
|
+
return null; // Will use fallback
|
|
1042
|
+
}
|
|
1043
|
+
const { mainFlow, screenPositions } = navigation;
|
|
1044
|
+
// Check if current screen is in the main flow
|
|
1045
|
+
const currentFlowIndex = mainFlow.indexOf(fromScreenId);
|
|
1046
|
+
if (currentFlowIndex !== -1) {
|
|
1047
|
+
// Navigate to next screen in main flow
|
|
1048
|
+
const nextFlowIndex = currentFlowIndex + 1;
|
|
1049
|
+
if (nextFlowIndex < mainFlow.length) {
|
|
1050
|
+
const nextScreenId = mainFlow[nextFlowIndex];
|
|
1051
|
+
console.log(`[RampKit] 🧭 __continue__ resolved via mainFlow: ${fromScreenId} → ${nextScreenId} (flow index ${currentFlowIndex} → ${nextFlowIndex})`);
|
|
1052
|
+
return nextScreenId;
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
console.log(`[RampKit] 🧭 __continue__: at end of mainFlow (index ${currentFlowIndex})`);
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
// Current screen is NOT in mainFlow (it's a variant screen)
|
|
1060
|
+
// Find the appropriate next main screen based on X position
|
|
1061
|
+
if (screenPositions) {
|
|
1062
|
+
const currentPosition = screenPositions[fromScreenId];
|
|
1063
|
+
if (currentPosition) {
|
|
1064
|
+
console.log(`[RampKit] 🧭 Current screen '${fromScreenId}' is a variant (row: ${currentPosition.row}, x: ${currentPosition.x})`);
|
|
1065
|
+
// Find the main flow screen that comes AFTER this X position
|
|
1066
|
+
// (the screen with the smallest X that is > currentPosition.x)
|
|
1067
|
+
let bestCandidate = null;
|
|
1068
|
+
for (const mainScreenId of mainFlow) {
|
|
1069
|
+
const mainPos = screenPositions[mainScreenId];
|
|
1070
|
+
if (mainPos && mainPos.x > currentPosition.x) {
|
|
1071
|
+
if (!bestCandidate || mainPos.x < bestCandidate.x) {
|
|
1072
|
+
bestCandidate = { screenId: mainScreenId, x: mainPos.x };
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if (bestCandidate) {
|
|
1077
|
+
console.log(`[RampKit] 🧭 __continue__ from variant: ${fromScreenId} → ${bestCandidate.screenId} (next main screen at x:${bestCandidate.x})`);
|
|
1078
|
+
return bestCandidate.screenId;
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
console.log("[RampKit] 🧭 __continue__ from variant: no main screen to the right, end of flow");
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
// Position data not available for current screen, fall back to array
|
|
1087
|
+
console.log(`[RampKit] 🧭 Screen '${fromScreenId}' not found in positions, using array fallback`);
|
|
1088
|
+
return null;
|
|
1089
|
+
};
|
|
1090
|
+
/**
|
|
1091
|
+
* Resolve `__goBack__` to the actual target screen ID using navigation data
|
|
1092
|
+
* @param fromScreenId - The current screen ID
|
|
1093
|
+
* @returns The target screen ID, or null if at the start of the flow or should use fallback
|
|
1094
|
+
*/
|
|
1095
|
+
const resolveGoBack = (fromScreenId) => {
|
|
1096
|
+
const navigation = props.navigation;
|
|
1097
|
+
// If no navigation data, fall back to array order
|
|
1098
|
+
if (!(navigation === null || navigation === void 0 ? void 0 : navigation.mainFlow) || navigation.mainFlow.length === 0) {
|
|
1099
|
+
console.log("[RampKit] 🧭 No navigation.mainFlow, using array order for __goBack__");
|
|
1100
|
+
return null; // Will use fallback
|
|
1101
|
+
}
|
|
1102
|
+
const { mainFlow, screenPositions } = navigation;
|
|
1103
|
+
// Check if current screen is in the main flow
|
|
1104
|
+
const currentFlowIndex = mainFlow.indexOf(fromScreenId);
|
|
1105
|
+
if (currentFlowIndex !== -1) {
|
|
1106
|
+
// Navigate to previous screen in main flow
|
|
1107
|
+
const prevFlowIndex = currentFlowIndex - 1;
|
|
1108
|
+
if (prevFlowIndex >= 0) {
|
|
1109
|
+
const prevScreenId = mainFlow[prevFlowIndex];
|
|
1110
|
+
console.log(`[RampKit] 🧭 __goBack__ resolved via mainFlow: ${fromScreenId} → ${prevScreenId} (flow index ${currentFlowIndex} → ${prevFlowIndex})`);
|
|
1111
|
+
return prevScreenId;
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
console.log(`[RampKit] 🧭 __goBack__: at start of mainFlow (index ${currentFlowIndex})`);
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
// Current screen is NOT in mainFlow (it's a variant screen)
|
|
1119
|
+
// Go back to the main flow screen at or before this X position
|
|
1120
|
+
if (screenPositions) {
|
|
1121
|
+
const currentPosition = screenPositions[fromScreenId];
|
|
1122
|
+
if (currentPosition) {
|
|
1123
|
+
console.log(`[RampKit] 🧭 Current screen '${fromScreenId}' is a variant (row: ${currentPosition.row}, x: ${currentPosition.x})`);
|
|
1124
|
+
// Find the main flow screen that is at or before this X position
|
|
1125
|
+
// (the screen with the largest X that is <= currentPosition.x)
|
|
1126
|
+
let bestCandidate = null;
|
|
1127
|
+
for (const mainScreenId of mainFlow) {
|
|
1128
|
+
const mainPos = screenPositions[mainScreenId];
|
|
1129
|
+
if (mainPos && mainPos.x <= currentPosition.x) {
|
|
1130
|
+
if (!bestCandidate || mainPos.x > bestCandidate.x) {
|
|
1131
|
+
bestCandidate = { screenId: mainScreenId, x: mainPos.x };
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (bestCandidate) {
|
|
1136
|
+
console.log(`[RampKit] 🧭 __goBack__ from variant: ${fromScreenId} → ${bestCandidate.screenId} (main screen at x:${bestCandidate.x})`);
|
|
1137
|
+
return bestCandidate.screenId;
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
console.log("[RampKit] 🧭 __goBack__ from variant: no main screen to the left, start of flow");
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
// Position data not available for current screen, fall back to array
|
|
1146
|
+
console.log(`[RampKit] 🧭 Screen '${fromScreenId}' not found in positions, using array fallback`);
|
|
1147
|
+
return null;
|
|
1148
|
+
};
|
|
1149
|
+
/**
|
|
1150
|
+
* Get the screen index for a given screen ID
|
|
1151
|
+
*/
|
|
1152
|
+
const getScreenIndex = (screenId) => {
|
|
1153
|
+
return props.screens.findIndex((s) => s.id === screenId);
|
|
1154
|
+
};
|
|
832
1155
|
// Fade-in when overlay becomes visible
|
|
833
1156
|
react_1.default.useEffect(() => {
|
|
834
1157
|
if (visible && !isClosing) {
|
|
@@ -1021,6 +1344,57 @@ function Overlay(props) {
|
|
|
1021
1344
|
}
|
|
1022
1345
|
})();`;
|
|
1023
1346
|
}
|
|
1347
|
+
// Build a script that updates the onboarding state
|
|
1348
|
+
// This calls window.__rampkitUpdateOnboarding(index, screenId) to update
|
|
1349
|
+
// onboarding.currentIndex, onboarding.progress, etc.
|
|
1350
|
+
function buildOnboardingStateScript(screenIndex, screenId, totalScreens) {
|
|
1351
|
+
return `(function() {
|
|
1352
|
+
try {
|
|
1353
|
+
// Set total screens global
|
|
1354
|
+
window.RK_TOTAL_SCREENS = ${totalScreens};
|
|
1355
|
+
|
|
1356
|
+
// Call the update function if it exists
|
|
1357
|
+
if (typeof window.__rampkitUpdateOnboarding === 'function') {
|
|
1358
|
+
window.__rampkitUpdateOnboarding(${screenIndex}, '${screenId}');
|
|
1359
|
+
console.log('[RampKit] Called __rampkitUpdateOnboarding(${screenIndex}, ${screenId})');
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Also dispatch a message event for any listeners
|
|
1363
|
+
var payload = {
|
|
1364
|
+
type: 'rampkit:onboarding-state',
|
|
1365
|
+
currentIndex: ${screenIndex},
|
|
1366
|
+
screenId: '${screenId}',
|
|
1367
|
+
totalScreens: ${totalScreens}
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
try {
|
|
1371
|
+
document.dispatchEvent(new MessageEvent('message', { data: payload }));
|
|
1372
|
+
} catch(e) {}
|
|
1373
|
+
|
|
1374
|
+
// Also dispatch custom event
|
|
1375
|
+
try {
|
|
1376
|
+
document.dispatchEvent(new CustomEvent('rampkit:onboarding-state', { detail: payload }));
|
|
1377
|
+
} catch(e) {}
|
|
1378
|
+
|
|
1379
|
+
} catch(e) {
|
|
1380
|
+
console.log('[RampKit] sendOnboardingState error:', e);
|
|
1381
|
+
}
|
|
1382
|
+
})();`;
|
|
1383
|
+
}
|
|
1384
|
+
// Send onboarding state to a WebView
|
|
1385
|
+
function sendOnboardingStateToWebView(i) {
|
|
1386
|
+
var _a;
|
|
1387
|
+
const wv = webviewsRef.current[i];
|
|
1388
|
+
if (!wv)
|
|
1389
|
+
return;
|
|
1390
|
+
const screenId = ((_a = props.screens[i]) === null || _a === void 0 ? void 0 : _a.id) || '';
|
|
1391
|
+
const totalScreens = props.screens.length;
|
|
1392
|
+
if (__DEV__) {
|
|
1393
|
+
console.log("[Rampkit] sendOnboardingStateToWebView", i, { screenId, totalScreens });
|
|
1394
|
+
}
|
|
1395
|
+
// @ts-ignore: injectJavaScript exists on WebView instance
|
|
1396
|
+
wv.injectJavaScript(buildOnboardingStateScript(i, screenId, totalScreens));
|
|
1397
|
+
}
|
|
1024
1398
|
function sendVarsToWebView(i, isInitialLoad = false) {
|
|
1025
1399
|
const wv = webviewsRef.current[i];
|
|
1026
1400
|
if (!wv)
|
|
@@ -1034,6 +1408,8 @@ function Overlay(props) {
|
|
|
1034
1408
|
// This is more reliable as it doesn't depend on event listeners being set up
|
|
1035
1409
|
// @ts-ignore: injectJavaScript exists on WebView instance
|
|
1036
1410
|
wv.injectJavaScript(buildDirectVarsScript(varsRef.current));
|
|
1411
|
+
// Also send onboarding state so templates like ${onboarding.progress} are resolved
|
|
1412
|
+
sendOnboardingStateToWebView(i);
|
|
1037
1413
|
}
|
|
1038
1414
|
/**
|
|
1039
1415
|
* Broadcast variables to all WebViews, optionally excluding one.
|
|
@@ -1130,8 +1506,24 @@ function Overlay(props) {
|
|
|
1130
1506
|
}
|
|
1131
1507
|
};
|
|
1132
1508
|
const handleAdvance = (i, animation = "fade") => {
|
|
1509
|
+
var _a;
|
|
1510
|
+
const currentScreenId = (_a = props.screens[i]) === null || _a === void 0 ? void 0 : _a.id;
|
|
1511
|
+
// Try to resolve using navigation data
|
|
1512
|
+
if (currentScreenId) {
|
|
1513
|
+
const resolvedId = resolveContinue(currentScreenId);
|
|
1514
|
+
if (resolvedId) {
|
|
1515
|
+
const targetIndex = getScreenIndex(resolvedId);
|
|
1516
|
+
if (targetIndex >= 0 && targetIndex < props.screens.length) {
|
|
1517
|
+
navigateToIndex(targetIndex, animation);
|
|
1518
|
+
RampKitNative_1.Haptics.impactAsync("light").catch(() => { });
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
// Fallback to array order
|
|
1133
1524
|
const last = props.screens.length - 1;
|
|
1134
1525
|
if (i < last) {
|
|
1526
|
+
console.log(`[RampKit] 🧭 __continue__ fallback to array index ${i + 1}`);
|
|
1135
1527
|
navigateToIndex(i + 1, animation);
|
|
1136
1528
|
RampKitNative_1.Haptics.impactAsync("light").catch(() => { });
|
|
1137
1529
|
}
|
|
@@ -1142,6 +1534,29 @@ function Overlay(props) {
|
|
|
1142
1534
|
handleRequestClose({ completed: true });
|
|
1143
1535
|
}
|
|
1144
1536
|
};
|
|
1537
|
+
const handleGoBack = (i, animation = "fade") => {
|
|
1538
|
+
var _a;
|
|
1539
|
+
const currentScreenId = (_a = props.screens[i]) === null || _a === void 0 ? void 0 : _a.id;
|
|
1540
|
+
// Try to resolve using navigation data
|
|
1541
|
+
if (currentScreenId) {
|
|
1542
|
+
const resolvedId = resolveGoBack(currentScreenId);
|
|
1543
|
+
if (resolvedId) {
|
|
1544
|
+
const targetIndex = getScreenIndex(resolvedId);
|
|
1545
|
+
if (targetIndex >= 0 && targetIndex < props.screens.length) {
|
|
1546
|
+
navigateToIndex(targetIndex, animation);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
// Fallback to array order
|
|
1552
|
+
if (i > 0) {
|
|
1553
|
+
console.log(`[RampKit] 🧭 __goBack__ fallback to array index ${i - 1}`);
|
|
1554
|
+
navigateToIndex(i - 1, animation);
|
|
1555
|
+
}
|
|
1556
|
+
else {
|
|
1557
|
+
handleRequestClose();
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1145
1560
|
async function handleNotificationPermissionRequest(payload) {
|
|
1146
1561
|
var _a, _b;
|
|
1147
1562
|
// Track that notification permission was requested
|
|
@@ -1210,7 +1625,7 @@ function Overlay(props) {
|
|
|
1210
1625
|
opacity: pagerOpacity,
|
|
1211
1626
|
transform: [{ translateX: pagerTranslateX }],
|
|
1212
1627
|
},
|
|
1213
|
-
], children: (0, jsx_runtime_1.jsx)(react_native_pager_view_1.default, { ref: pagerRef, style: react_native_1.StyleSheet.absoluteFill, scrollEnabled: false, initialPage: 0, onPageSelected: onPageSelected, offscreenPageLimit: props.screens.length, overScrollMode: "never", children: docs.map((doc, i) => ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.page, renderToHardwareTextureAndroid: true, children: (0, jsx_runtime_1.jsx)(react_native_webview_1.WebView, { ref: (r) => (webviewsRef.current[i] = r), style: styles.webview, originWhitelist: ["*"], source: { html: doc }, injectedJavaScriptBeforeContentLoaded: exports.injectedHardening + exports.injectedButtonAnimations, injectedJavaScript: exports.injectedNoSelect + exports.injectedVarsHandler + exports.injectedButtonAnimations, automaticallyAdjustContentInsets: false, contentInsetAdjustmentBehavior: "never", bounces: false, scrollEnabled: false, overScrollMode: "never", scalesPageToFit: false, showsHorizontalScrollIndicator: false, dataDetectorTypes: "none", allowsLinkPreview: false, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserAction: false, cacheEnabled: true, javaScriptEnabled: true, domStorageEnabled: true, hideKeyboardAccessoryView: true, onLoadEnd: () => {
|
|
1628
|
+
], children: (0, jsx_runtime_1.jsx)(react_native_pager_view_1.default, { ref: pagerRef, style: react_native_1.StyleSheet.absoluteFill, scrollEnabled: false, initialPage: 0, onPageSelected: onPageSelected, offscreenPageLimit: props.screens.length, overScrollMode: "never", children: docs.map((doc, i) => ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.page, renderToHardwareTextureAndroid: true, children: (0, jsx_runtime_1.jsx)(react_native_webview_1.WebView, { ref: (r) => (webviewsRef.current[i] = r), style: styles.webview, originWhitelist: ["*"], source: { html: doc }, injectedJavaScriptBeforeContentLoaded: exports.injectedHardening + exports.injectedDynamicTapHandler + exports.injectedButtonAnimations, injectedJavaScript: exports.injectedNoSelect + exports.injectedVarsHandler + exports.injectedButtonAnimations, automaticallyAdjustContentInsets: false, contentInsetAdjustmentBehavior: "never", bounces: false, scrollEnabled: false, overScrollMode: "never", scalesPageToFit: false, showsHorizontalScrollIndicator: false, dataDetectorTypes: "none", allowsLinkPreview: false, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserAction: false, cacheEnabled: true, javaScriptEnabled: true, domStorageEnabled: true, hideKeyboardAccessoryView: true, onLoadEnd: () => {
|
|
1214
1629
|
setLoadedCount((c) => c + 1);
|
|
1215
1630
|
if (i === 0) {
|
|
1216
1631
|
setFirstPageLoaded(true);
|
|
@@ -1284,6 +1699,9 @@ function Overlay(props) {
|
|
|
1284
1699
|
// This prevents echo loops and matches iOS SDK behavior
|
|
1285
1700
|
broadcastVars(i);
|
|
1286
1701
|
}
|
|
1702
|
+
// CRITICAL: Also send merged vars BACK to source page
|
|
1703
|
+
// This ensures window.__rampkitVariables is updated for dynamic tap evaluation
|
|
1704
|
+
sendVarsToWebView(i);
|
|
1287
1705
|
return;
|
|
1288
1706
|
}
|
|
1289
1707
|
// 2) A page asked for current vars → send only to that page
|
|
@@ -1342,12 +1760,7 @@ function Overlay(props) {
|
|
|
1342
1760
|
if ((data === null || data === void 0 ? void 0 : data.type) === "rampkit:navigate") {
|
|
1343
1761
|
const target = data === null || data === void 0 ? void 0 : data.targetScreenId;
|
|
1344
1762
|
if (target === "__goBack__") {
|
|
1345
|
-
|
|
1346
|
-
navigateToIndex(i - 1, (data === null || data === void 0 ? void 0 : data.animation) || "fade");
|
|
1347
|
-
}
|
|
1348
|
-
else {
|
|
1349
|
-
handleRequestClose();
|
|
1350
|
-
}
|
|
1763
|
+
handleGoBack(i, (data === null || data === void 0 ? void 0 : data.animation) || "fade");
|
|
1351
1764
|
return;
|
|
1352
1765
|
}
|
|
1353
1766
|
if (!target || target === "__continue__") {
|
|
@@ -1364,12 +1777,7 @@ function Overlay(props) {
|
|
|
1364
1777
|
return;
|
|
1365
1778
|
}
|
|
1366
1779
|
if ((data === null || data === void 0 ? void 0 : data.type) === "rampkit:goBack") {
|
|
1367
|
-
|
|
1368
|
-
navigateToIndex(i - 1, (data === null || data === void 0 ? void 0 : data.animation) || "fade");
|
|
1369
|
-
}
|
|
1370
|
-
else {
|
|
1371
|
-
handleRequestClose();
|
|
1372
|
-
}
|
|
1780
|
+
handleGoBack(i, (data === null || data === void 0 ? void 0 : data.animation) || "fade");
|
|
1373
1781
|
return;
|
|
1374
1782
|
}
|
|
1375
1783
|
if ((data === null || data === void 0 ? void 0 : data.type) === "rampkit:close") {
|
|
@@ -1422,23 +1830,13 @@ function Overlay(props) {
|
|
|
1422
1830
|
return;
|
|
1423
1831
|
}
|
|
1424
1832
|
if (raw === "rampkit:goBack") {
|
|
1425
|
-
|
|
1426
|
-
navigateToIndex(i - 1);
|
|
1427
|
-
}
|
|
1428
|
-
else {
|
|
1429
|
-
handleRequestClose();
|
|
1430
|
-
}
|
|
1833
|
+
handleGoBack(i);
|
|
1431
1834
|
return;
|
|
1432
1835
|
}
|
|
1433
1836
|
if (raw.startsWith("rampkit:navigate:")) {
|
|
1434
1837
|
const target = raw.slice("rampkit:navigate:".length);
|
|
1435
1838
|
if (target === "__goBack__") {
|
|
1436
|
-
|
|
1437
|
-
navigateToIndex(i - 1);
|
|
1438
|
-
}
|
|
1439
|
-
else {
|
|
1440
|
-
handleRequestClose();
|
|
1441
|
-
}
|
|
1839
|
+
handleGoBack(i);
|
|
1442
1840
|
return;
|
|
1443
1841
|
}
|
|
1444
1842
|
if (!target || target === "__continue__") {
|
package/build/index.d.ts
CHANGED
|
@@ -11,5 +11,5 @@ export { default as RampKitNative } from "./RampKitNative";
|
|
|
11
11
|
export type { NativeDeviceInfo, NativeLaunchData } from "./RampKitNative";
|
|
12
12
|
export { Haptics, StoreReview, Notifications, TransactionObserver } from "./RampKitNative";
|
|
13
13
|
export type { ImpactStyle, NotificationType, NotificationOptions, NotificationPermissionResult } from "./RampKitNative";
|
|
14
|
-
export type { DeviceInfo, RampKitEvent, EventDevice, EventContext, RampKitConfig, RampKitEventName, RampKitContext, RampKitDeviceContext, RampKitUserContext, AppSessionStartedProperties, AppSessionEndedProperties, AppBackgroundedProperties, AppForegroundedProperties, OnboardingStartedProperties, OnboardingScreenViewedProperties, OnboardingQuestionAnsweredProperties, OnboardingCompletedProperties, OnboardingAbandonedProperties, ScreenViewProperties, CtaTapProperties, NotificationsPromptShownProperties, NotificationsResponseProperties, PaywallShownProperties, PaywallPrimaryActionTapProperties, PaywallClosedProperties, PurchaseStartedProperties, PurchaseCompletedProperties, PurchaseFailedProperties, } from "./types";
|
|
14
|
+
export type { DeviceInfo, RampKitEvent, EventDevice, EventContext, RampKitConfig, RampKitEventName, RampKitContext, RampKitDeviceContext, RampKitUserContext, NavigationData, ScreenPosition, AppSessionStartedProperties, AppSessionEndedProperties, AppBackgroundedProperties, AppForegroundedProperties, OnboardingStartedProperties, OnboardingScreenViewedProperties, OnboardingQuestionAnsweredProperties, OnboardingCompletedProperties, OnboardingAbandonedProperties, ScreenViewProperties, CtaTapProperties, NotificationsPromptShownProperties, NotificationsResponseProperties, PaywallShownProperties, PaywallPrimaryActionTapProperties, PaywallClosedProperties, PurchaseStartedProperties, PurchaseCompletedProperties, PurchaseFailedProperties, } from "./types";
|
|
15
15
|
export { SDK_VERSION, CAPABILITIES } from "./constants";
|
package/build/types.d.ts
CHANGED
|
@@ -229,3 +229,24 @@ export interface RampKitContext {
|
|
|
229
229
|
device: RampKitDeviceContext;
|
|
230
230
|
user: RampKitUserContext;
|
|
231
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Navigation data structure from the editor's spatial layout
|
|
234
|
+
* This is exported alongside screens to enable proper navigation resolution
|
|
235
|
+
*/
|
|
236
|
+
export interface NavigationData {
|
|
237
|
+
/** Ordered array of screen IDs in the main flow (sorted by X position, main row only) */
|
|
238
|
+
mainFlow: string[];
|
|
239
|
+
/** Map of screen ID to position information */
|
|
240
|
+
screenPositions?: Record<string, ScreenPosition>;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Position information for a screen in the editor canvas
|
|
244
|
+
*/
|
|
245
|
+
export interface ScreenPosition {
|
|
246
|
+
/** X coordinate in the editor canvas */
|
|
247
|
+
x: number;
|
|
248
|
+
/** Y coordinate in the editor canvas */
|
|
249
|
+
y: number;
|
|
250
|
+
/** Row classification: "main" for main row screens, "variant" for screens below */
|
|
251
|
+
row: "main" | "variant";
|
|
252
|
+
}
|
package/ios/RampKitModule.swift
CHANGED
|
@@ -458,63 +458,86 @@ public class RampKitModule: Module {
|
|
|
458
458
|
@available(iOS 15.0, *)
|
|
459
459
|
private func handleTransaction(_ transaction: Transaction) async {
|
|
460
460
|
guard let appId = self.appId, let userId = self.userId else { return }
|
|
461
|
-
|
|
462
|
-
|
|
461
|
+
|
|
462
|
+
let formatter = ISO8601DateFormatter()
|
|
463
|
+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
464
|
+
|
|
465
|
+
// Determine event type based on transaction (matching iOS SDK logic)
|
|
463
466
|
let eventName: String
|
|
464
467
|
var properties: [String: Any] = [:]
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
eventName = "
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if transaction.isUpgraded {
|
|
472
|
-
eventName = "subscription_upgraded"
|
|
473
|
-
} else {
|
|
474
|
-
// Determine event based on product type
|
|
475
|
-
let productType = transaction.productType
|
|
476
|
-
if productType == .autoRenewable {
|
|
477
|
-
if transaction.originalID == transaction.id {
|
|
478
|
-
eventName = "purchase_completed"
|
|
479
|
-
} else {
|
|
480
|
-
eventName = "subscription_renewed"
|
|
481
|
-
}
|
|
482
|
-
} else {
|
|
483
|
-
eventName = "purchase_completed"
|
|
484
|
-
}
|
|
468
|
+
|
|
469
|
+
// Check revocation first (subscription_canceled)
|
|
470
|
+
if transaction.revocationDate != nil {
|
|
471
|
+
eventName = "subscription_canceled"
|
|
472
|
+
if let reason = transaction.revocationReason {
|
|
473
|
+
properties["revocationReason"] = reason == .developerIssue ? "developerIssue" : "other"
|
|
485
474
|
}
|
|
475
|
+
properties["revocationDate"] = transaction.revocationDate.map { formatter.string(from: $0) }
|
|
486
476
|
}
|
|
487
|
-
|
|
488
|
-
|
|
477
|
+
// Check if this is a renewal (originalID != id)
|
|
478
|
+
else if transaction.originalID != transaction.id {
|
|
479
|
+
eventName = "subscription_renewed"
|
|
480
|
+
}
|
|
481
|
+
// Check if it's a trial or intro offer
|
|
482
|
+
else if let offerType = transaction.offerType, offerType == .introductory {
|
|
483
|
+
eventName = "trial_started"
|
|
484
|
+
}
|
|
485
|
+
// Default to purchase_completed
|
|
486
|
+
else {
|
|
487
|
+
eventName = "purchase_completed"
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Build properties (matching iOS SDK PurchaseEventDetails)
|
|
489
491
|
properties["productId"] = transaction.productID
|
|
490
492
|
properties["transactionId"] = String(transaction.id)
|
|
491
493
|
properties["originalTransactionId"] = String(transaction.originalID)
|
|
492
|
-
properties["purchaseDate"] =
|
|
493
|
-
|
|
494
|
-
if let expirationDate = transaction.expirationDate {
|
|
495
|
-
properties["expirationDate"] = ISO8601DateFormatter().string(from: expirationDate)
|
|
496
|
-
}
|
|
497
|
-
|
|
494
|
+
properties["purchaseDate"] = formatter.string(from: transaction.purchaseDate)
|
|
498
495
|
properties["quantity"] = transaction.purchasedQuantity
|
|
499
496
|
properties["productType"] = mapProductType(transaction.productType)
|
|
500
|
-
|
|
501
|
-
//
|
|
502
|
-
if
|
|
503
|
-
properties["
|
|
497
|
+
|
|
498
|
+
// Expiration date
|
|
499
|
+
if let expirationDate = transaction.expirationDate {
|
|
500
|
+
properties["expirationDate"] = formatter.string(from: expirationDate)
|
|
504
501
|
}
|
|
505
|
-
|
|
502
|
+
|
|
503
|
+
// Trial/intro offer detection
|
|
504
|
+
if let offerType = transaction.offerType {
|
|
505
|
+
properties["isTrial"] = offerType == .introductory
|
|
506
|
+
properties["isIntroOffer"] = offerType == .introductory
|
|
507
|
+
properties["offerType"] = formatOfferType(offerType)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Offer ID
|
|
511
|
+
if let offerId = transaction.offerID {
|
|
512
|
+
properties["offerId"] = offerId
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Storefront country
|
|
516
|
+
properties["storefront"] = transaction.storefrontCountryCode
|
|
517
|
+
|
|
518
|
+
// Web order line item ID
|
|
506
519
|
if let webOrderLineItemID = transaction.webOrderLineItemID {
|
|
507
520
|
properties["webOrderLineItemId"] = webOrderLineItemID
|
|
508
521
|
}
|
|
509
|
-
|
|
522
|
+
|
|
523
|
+
// Environment (iOS 16+)
|
|
524
|
+
if #available(iOS 16.0, *) {
|
|
525
|
+
properties["environment"] = transaction.environment.rawValue
|
|
526
|
+
}
|
|
527
|
+
|
|
510
528
|
// Get price info from product if available
|
|
511
529
|
if let product = await getProduct(for: transaction.productID) {
|
|
512
|
-
properties["
|
|
530
|
+
properties["amount"] = product.price
|
|
513
531
|
properties["currency"] = product.priceFormatStyle.currencyCode
|
|
514
|
-
properties["
|
|
515
|
-
|
|
532
|
+
properties["priceFormatted"] = product.displayPrice
|
|
533
|
+
|
|
534
|
+
// Subscription-specific info
|
|
535
|
+
if let subscription = product.subscription {
|
|
536
|
+
properties["subscriptionPeriod"] = formatSubscriptionPeriod(subscription.subscriptionPeriod)
|
|
537
|
+
properties["subscriptionGroupId"] = subscription.subscriptionGroupID
|
|
538
|
+
}
|
|
516
539
|
}
|
|
517
|
-
|
|
540
|
+
|
|
518
541
|
// Send event to backend
|
|
519
542
|
await sendPurchaseEvent(
|
|
520
543
|
appId: appId,
|
|
@@ -523,6 +546,36 @@ public class RampKitModule: Module {
|
|
|
523
546
|
properties: properties
|
|
524
547
|
)
|
|
525
548
|
}
|
|
549
|
+
|
|
550
|
+
@available(iOS 15.0, *)
|
|
551
|
+
private func formatOfferType(_ offerType: Transaction.OfferType) -> String {
|
|
552
|
+
if offerType == .introductory {
|
|
553
|
+
return "introductory"
|
|
554
|
+
} else if offerType == .promotional {
|
|
555
|
+
return "promotional"
|
|
556
|
+
} else if offerType == .code {
|
|
557
|
+
return "code"
|
|
558
|
+
} else {
|
|
559
|
+
return "unknown"
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
@available(iOS 15.0, *)
|
|
564
|
+
private func formatSubscriptionPeriod(_ period: Product.SubscriptionPeriod) -> String {
|
|
565
|
+
// ISO 8601 duration format
|
|
566
|
+
let unit = period.unit
|
|
567
|
+
if unit == .day {
|
|
568
|
+
return "P\(period.value)D"
|
|
569
|
+
} else if unit == .week {
|
|
570
|
+
return "P\(period.value)W"
|
|
571
|
+
} else if unit == .month {
|
|
572
|
+
return "P\(period.value)M"
|
|
573
|
+
} else if unit == .year {
|
|
574
|
+
return "P\(period.value)Y"
|
|
575
|
+
} else {
|
|
576
|
+
return "P\(period.value)D"
|
|
577
|
+
}
|
|
578
|
+
}
|
|
526
579
|
|
|
527
580
|
@available(iOS 15.0, *)
|
|
528
581
|
private func getProduct(for productId: String) async -> Product? {
|
package/package.json
CHANGED