@namiml/expo-sdk 3.4.0-dev.202605060437
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/dist/index.cjs +4000 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.mjs +3966 -0
- package/dist/index.mjs.map +1 -0
- package/nami-expo-nami-iap.tgz +0 -0
- package/package.json +92 -0
- package/src/adapters/expo-device.adapter.ts +106 -0
- package/src/adapters/expo-purchase.adapter.ts +79 -0
- package/src/adapters/expo-storage.adapter.ts +92 -0
- package/src/adapters/expo-ui.adapter.ts +57 -0
- package/src/adapters/index.ts +33 -0
- package/src/amazon-kepler.d.ts +7 -0
- package/src/components/NamiView.tsx +1006 -0
- package/src/components/PaywallScreen.tsx +245 -0
- package/src/components/TemplateRenderer.tsx +243 -0
- package/src/components/containers/NamiBackgroundContainer.tsx +103 -0
- package/src/components/containers/NamiCarousel.tsx +217 -0
- package/src/components/containers/NamiCollapseContainer.tsx +116 -0
- package/src/components/containers/NamiContainer.tsx +315 -0
- package/src/components/containers/NamiContentContainer.tsx +140 -0
- package/src/components/containers/NamiFooter.tsx +35 -0
- package/src/components/containers/NamiHeader.tsx +45 -0
- package/src/components/containers/NamiProductContainer.tsx +248 -0
- package/src/components/containers/NamiRepeatingGrid.tsx +81 -0
- package/src/components/containers/NamiResponsiveGrid.tsx +75 -0
- package/src/components/containers/NamiStack.tsx +69 -0
- package/src/components/elements/NamiButton.tsx +285 -0
- package/src/components/elements/NamiCountdownTimer.tsx +123 -0
- package/src/components/elements/NamiImage.tsx +177 -0
- package/src/components/elements/NamiPlayPauseButton.tsx +93 -0
- package/src/components/elements/NamiProgressBar.tsx +90 -0
- package/src/components/elements/NamiProgressIndicator.tsx +41 -0
- package/src/components/elements/NamiQRCode.tsx +51 -0
- package/src/components/elements/NamiRadioButton.tsx +62 -0
- package/src/components/elements/NamiSegmentPicker.tsx +67 -0
- package/src/components/elements/NamiSegmentPickerItem.tsx +184 -0
- package/src/components/elements/NamiSpacer.tsx +23 -0
- package/src/components/elements/NamiSymbol.tsx +104 -0
- package/src/components/elements/NamiText.tsx +311 -0
- package/src/components/elements/NamiToggleButton.tsx +102 -0
- package/src/components/elements/NamiToggleSwitch.tsx +64 -0
- package/src/components/elements/NamiVideo.kepler.tsx +638 -0
- package/src/components/elements/NamiVideo.tsx +133 -0
- package/src/components/elements/NamiVolumeButton.tsx +93 -0
- package/src/context/FocusContext.tsx +169 -0
- package/src/context/PaywallContext.tsx +343 -0
- package/src/global.d.ts +5 -0
- package/src/index.ts +62 -0
- package/src/nami.ts +24 -0
- package/src/react-native-qrcode-svg.d.ts +4 -0
- package/src/utils/actionHandler.ts +281 -0
- package/src/utils/fonts.ts +359 -0
- package/src/utils/iconMap.ts +67 -0
- package/src/utils/impression.ts +39 -0
- package/src/utils/rendering.ts +197 -0
- package/src/utils/smartText.ts +148 -0
- package/src/utils/styles.ts +668 -0
- package/src/utils/tvFocus.ts +31 -0
- package/src/utils/videoControls.ts +49 -0
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
import React, { useState, useEffect, useLayoutEffect, useRef, useMemo, useCallback } from 'react';
|
|
2
|
+
import { View, StyleSheet, Animated, Dimensions, SafeAreaView, StatusBar, BackHandler, ActivityIndicator } from 'react-native';
|
|
3
|
+
import type {
|
|
4
|
+
IPaywall,
|
|
5
|
+
NamiPaywallLaunchContext,
|
|
6
|
+
NamiCampaign,
|
|
7
|
+
FlowNavigationOptions,
|
|
8
|
+
NamiFlow,
|
|
9
|
+
NamiPaywallEvent,
|
|
10
|
+
} from '@namiml/sdk-core';
|
|
11
|
+
import {
|
|
12
|
+
getPaywallDataFromLabel,
|
|
13
|
+
getPaywall,
|
|
14
|
+
isNamiFlowCampaign,
|
|
15
|
+
NamiFlowManager,
|
|
16
|
+
NamiReservedActions,
|
|
17
|
+
hasAllPaywalls,
|
|
18
|
+
NamiEventEmitter,
|
|
19
|
+
NamiPaywallAction,
|
|
20
|
+
PAYWALL_ACTION_EVENT,
|
|
21
|
+
logger,
|
|
22
|
+
} from '@namiml/sdk-core';
|
|
23
|
+
import { PaywallProvider } from '../context/PaywallContext';
|
|
24
|
+
import { PaywallScreen } from './PaywallScreen';
|
|
25
|
+
import { expoUIAdapter } from '../adapters';
|
|
26
|
+
import { FocusProvider } from '../context/FocusContext';
|
|
27
|
+
import { prewarmPaywallFonts, prepareAndLoadFontsWithTimeout } from '../utils/fonts';
|
|
28
|
+
import { parseColor } from '../utils/styles';
|
|
29
|
+
|
|
30
|
+
export interface NamiViewProps {
|
|
31
|
+
/** Campaign placement label */
|
|
32
|
+
placement?: string;
|
|
33
|
+
/** Campaign URL for deep-link resolution */
|
|
34
|
+
url?: string;
|
|
35
|
+
/** Called when the paywall should be dismissed */
|
|
36
|
+
onClose?: () => void;
|
|
37
|
+
/** Called when the user taps the sign-in action */
|
|
38
|
+
onSignIn?: () => void;
|
|
39
|
+
/** Called when a deep link action fires */
|
|
40
|
+
onDeepLink?: (url: string) => void;
|
|
41
|
+
/** Called when a purchase is initiated */
|
|
42
|
+
onPurchase?: (sku: any) => void;
|
|
43
|
+
/** Called when the user taps the restore purchases action */
|
|
44
|
+
onRestore?: () => void;
|
|
45
|
+
/** Catch-all callback for every paywall action event */
|
|
46
|
+
onPaywallEvent?: (event: NamiPaywallEvent) => void;
|
|
47
|
+
/** Called when a flow handoff step fires */
|
|
48
|
+
onHandoff?: (handoffTag: string, handoffData?: Record<string, any>) => void;
|
|
49
|
+
/** Called when a flow event fires */
|
|
50
|
+
onFlowEvent?: (event: Record<string, any>) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type LaunchRequest = {
|
|
54
|
+
type?: string;
|
|
55
|
+
value: string;
|
|
56
|
+
context?: NamiPaywallLaunchContext;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type FlowState = {
|
|
60
|
+
paywalls: IPaywall[];
|
|
61
|
+
index: number;
|
|
62
|
+
animation?: FlowNavigationOptions | null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type CachedLaunch = {
|
|
66
|
+
paywallData: IPaywall | null;
|
|
67
|
+
campaignData: NamiCampaign | null;
|
|
68
|
+
launchPreviewPaywall: IPaywall | null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type RemoteBackFallbackAttempt = {
|
|
72
|
+
flow: NamiFlow;
|
|
73
|
+
stepId: string;
|
|
74
|
+
paywallId?: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const DEFAULT_CONTEXT: NamiPaywallLaunchContext = {
|
|
78
|
+
productGroups: [],
|
|
79
|
+
customAttributes: {},
|
|
80
|
+
customObject: {},
|
|
81
|
+
currentGroup: '',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const REMOTE_BACK_FALLBACK_DELAY_MS = 600;
|
|
85
|
+
|
|
86
|
+
export const NamiView: React.FC<NamiViewProps> = ({
|
|
87
|
+
placement,
|
|
88
|
+
url,
|
|
89
|
+
onClose,
|
|
90
|
+
onSignIn,
|
|
91
|
+
onDeepLink,
|
|
92
|
+
onPurchase,
|
|
93
|
+
onRestore,
|
|
94
|
+
onPaywallEvent,
|
|
95
|
+
onHandoff,
|
|
96
|
+
onFlowEvent,
|
|
97
|
+
}) => {
|
|
98
|
+
const [launchRequest, setLaunchRequest] = useState<LaunchRequest | null>(null);
|
|
99
|
+
const [reloadKey, setReloadKey] = useState(0);
|
|
100
|
+
const [isClosing, setIsClosing] = useState(false);
|
|
101
|
+
|
|
102
|
+
const [paywallData, setPaywallData] = useState<IPaywall | null>(null);
|
|
103
|
+
const [campaignData, setCampaignData] = useState<NamiCampaign | null>(null);
|
|
104
|
+
const [launchPreviewPaywall, setLaunchPreviewPaywall] = useState<IPaywall | null>(null);
|
|
105
|
+
const [pendingTransitionPaywall, setPendingTransitionPaywall] = useState<IPaywall | null>(null);
|
|
106
|
+
const [initialScreenCommitted, setInitialScreenCommitted] = useState(false);
|
|
107
|
+
const [loading, setLoading] = useState(true);
|
|
108
|
+
|
|
109
|
+
const [flowState, setFlowState] = useState<FlowState>({ paywalls: [], index: 0, animation: null });
|
|
110
|
+
const flowRef = useRef<NamiFlow | undefined>();
|
|
111
|
+
const lastLaunchKeyRef = useRef<string>('');
|
|
112
|
+
const lastAppearStepIdRef = useRef<string>('');
|
|
113
|
+
const launchCacheRef = useRef<Map<string, CachedLaunch>>(new Map());
|
|
114
|
+
const remoteBackFallbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
115
|
+
const remoteBackFallbackAttemptRef = useRef<RemoteBackFallbackAttempt | null>(null);
|
|
116
|
+
const pendingNavigationFrameRef = useRef<number | null>(null);
|
|
117
|
+
|
|
118
|
+
// Stable refs for callbacks so event listeners always see latest values
|
|
119
|
+
const onCloseRef = useRef(onClose);
|
|
120
|
+
const onSignInRef = useRef(onSignIn);
|
|
121
|
+
const onDeepLinkRef = useRef(onDeepLink);
|
|
122
|
+
const onPurchaseRef = useRef(onPurchase);
|
|
123
|
+
const onRestoreRef = useRef(onRestore);
|
|
124
|
+
const onPaywallEventRef = useRef(onPaywallEvent);
|
|
125
|
+
const onHandoffRef = useRef(onHandoff);
|
|
126
|
+
const onFlowEventRef = useRef(onFlowEvent);
|
|
127
|
+
|
|
128
|
+
useEffect(() => { onCloseRef.current = onClose; }, [onClose]);
|
|
129
|
+
useEffect(() => { onSignInRef.current = onSignIn; }, [onSignIn]);
|
|
130
|
+
useEffect(() => { onDeepLinkRef.current = onDeepLink; }, [onDeepLink]);
|
|
131
|
+
useEffect(() => { onPurchaseRef.current = onPurchase; }, [onPurchase]);
|
|
132
|
+
useEffect(() => { onRestoreRef.current = onRestore; }, [onRestore]);
|
|
133
|
+
useEffect(() => { onPaywallEventRef.current = onPaywallEvent; }, [onPaywallEvent]);
|
|
134
|
+
useEffect(() => { onHandoffRef.current = onHandoff; }, [onHandoff]);
|
|
135
|
+
useEffect(() => { onFlowEventRef.current = onFlowEvent; }, [onFlowEvent]);
|
|
136
|
+
|
|
137
|
+
const resolvedLaunch = useMemo<LaunchRequest>(() => {
|
|
138
|
+
if (launchRequest?.value) return launchRequest;
|
|
139
|
+
if (url) return { type: 'url', value: url };
|
|
140
|
+
if (placement) return { type: 'label', value: placement };
|
|
141
|
+
return { type: undefined, value: '' };
|
|
142
|
+
}, [launchRequest, url, placement]);
|
|
143
|
+
|
|
144
|
+
const ctx = useMemo<NamiPaywallLaunchContext>(
|
|
145
|
+
() => ({
|
|
146
|
+
...DEFAULT_CONTEXT,
|
|
147
|
+
...(resolvedLaunch.context ?? launchRequest?.context ?? {}),
|
|
148
|
+
customAttributes: {
|
|
149
|
+
...DEFAULT_CONTEXT.customAttributes,
|
|
150
|
+
...(resolvedLaunch.context?.customAttributes ??
|
|
151
|
+
launchRequest?.context?.customAttributes ??
|
|
152
|
+
{}),
|
|
153
|
+
},
|
|
154
|
+
customObject: {
|
|
155
|
+
...DEFAULT_CONTEXT.customObject,
|
|
156
|
+
...(resolvedLaunch.context?.customObject ??
|
|
157
|
+
launchRequest?.context?.customObject ??
|
|
158
|
+
{}),
|
|
159
|
+
},
|
|
160
|
+
productGroups:
|
|
161
|
+
resolvedLaunch.context?.productGroups ??
|
|
162
|
+
launchRequest?.context?.productGroups ??
|
|
163
|
+
DEFAULT_CONTEXT.productGroups,
|
|
164
|
+
}),
|
|
165
|
+
[launchRequest?.context, resolvedLaunch.context],
|
|
166
|
+
);
|
|
167
|
+
const isFlowCampaign = isNamiFlowCampaign(campaignData as any);
|
|
168
|
+
|
|
169
|
+
const clearRemoteBackFallback = useCallback(() => {
|
|
170
|
+
if (remoteBackFallbackTimeoutRef.current) {
|
|
171
|
+
clearTimeout(remoteBackFallbackTimeoutRef.current);
|
|
172
|
+
remoteBackFallbackTimeoutRef.current = null;
|
|
173
|
+
}
|
|
174
|
+
remoteBackFallbackAttemptRef.current = null;
|
|
175
|
+
}, []);
|
|
176
|
+
|
|
177
|
+
const finishActiveFlow = useCallback(() => {
|
|
178
|
+
clearRemoteBackFallback();
|
|
179
|
+
const activeFlow = flowRef.current;
|
|
180
|
+
if (!activeFlow) return;
|
|
181
|
+
if (NamiFlowManager.instance.currentFlow === activeFlow || NamiFlowManager.instance.flowOpen) {
|
|
182
|
+
NamiFlowManager.finish();
|
|
183
|
+
}
|
|
184
|
+
flowRef.current = undefined;
|
|
185
|
+
lastAppearStepIdRef.current = '';
|
|
186
|
+
}, [clearRemoteBackFallback]);
|
|
187
|
+
|
|
188
|
+
const requestClose = useCallback(() => {
|
|
189
|
+
clearRemoteBackFallback();
|
|
190
|
+
if (onCloseRef.current) {
|
|
191
|
+
setIsClosing(true);
|
|
192
|
+
onCloseRef.current();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}, [clearRemoteBackFallback]);
|
|
196
|
+
|
|
197
|
+
const scheduleRemoteBackFallbackClose = useCallback((flow: NamiFlow, stepId: string, paywallId?: string) => {
|
|
198
|
+
clearRemoteBackFallback();
|
|
199
|
+
remoteBackFallbackAttemptRef.current = {
|
|
200
|
+
flow,
|
|
201
|
+
stepId,
|
|
202
|
+
paywallId,
|
|
203
|
+
};
|
|
204
|
+
remoteBackFallbackTimeoutRef.current = setTimeout(() => {
|
|
205
|
+
remoteBackFallbackTimeoutRef.current = null;
|
|
206
|
+
const attempt = remoteBackFallbackAttemptRef.current;
|
|
207
|
+
const activeFlow = flowRef.current;
|
|
208
|
+
const activeStepId = activeFlow?.currentFlowStep?.id;
|
|
209
|
+
const activeStepType = activeFlow?.currentFlowStep?.type;
|
|
210
|
+
const activePaywallId = paywallData?.id;
|
|
211
|
+
const transitionedToTerminalExit =
|
|
212
|
+
!!attempt
|
|
213
|
+
&& attempt.flow === activeFlow
|
|
214
|
+
&& activePaywallId === attempt.paywallId
|
|
215
|
+
&& (
|
|
216
|
+
activeFlow?.currentFlowStep?.type === 'exit'
|
|
217
|
+
|| NamiFlowManager.instance.flowOpen === false
|
|
218
|
+
);
|
|
219
|
+
const shouldFallbackClose =
|
|
220
|
+
!isClosing
|
|
221
|
+
&& !!attempt
|
|
222
|
+
&& (
|
|
223
|
+
(
|
|
224
|
+
attempt.flow === activeFlow
|
|
225
|
+
&& activeFlow?.currentFlowStep?.id === attempt.stepId
|
|
226
|
+
&& activePaywallId === attempt.paywallId
|
|
227
|
+
&& activeFlow?.previousStepAvailable === false
|
|
228
|
+
)
|
|
229
|
+
|| transitionedToTerminalExit
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
if (!shouldFallbackClose) {
|
|
233
|
+
remoteBackFallbackAttemptRef.current = null;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
remoteBackFallbackAttemptRef.current = null;
|
|
238
|
+
finishActiveFlow();
|
|
239
|
+
requestClose();
|
|
240
|
+
}, REMOTE_BACK_FALLBACK_DELAY_MS);
|
|
241
|
+
}, [clearRemoteBackFallback, finishActiveFlow, isClosing, paywallData?.id, requestClose]);
|
|
242
|
+
|
|
243
|
+
const handleFlowNavigation = useCallback(
|
|
244
|
+
(paywall: IPaywall, options: FlowNavigationOptions) => {
|
|
245
|
+
const flow = flowRef.current;
|
|
246
|
+
const currentPaywall =
|
|
247
|
+
flowState.paywalls[flowState.index] ??
|
|
248
|
+
flowState.paywalls[flowState.paywalls.length - 1] ??
|
|
249
|
+
paywallData;
|
|
250
|
+
const shouldUseAnimatedTransition =
|
|
251
|
+
options.transition !== 'none'
|
|
252
|
+
&& !!currentPaywall
|
|
253
|
+
&& currentPaywall.id !== paywall.id;
|
|
254
|
+
|
|
255
|
+
const applyNavigation = () => {
|
|
256
|
+
if (flow?.previousFlowStep) {
|
|
257
|
+
flow.executeLifecycle(
|
|
258
|
+
flow.previousFlowStep,
|
|
259
|
+
NamiReservedActions.DISAPPEAR,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
setFlowState(() => {
|
|
264
|
+
if (!shouldUseAnimatedTransition || !currentPaywall) {
|
|
265
|
+
return { paywalls: [paywall], index: 0, animation: options };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
paywalls: [currentPaywall, paywall],
|
|
270
|
+
index: 1,
|
|
271
|
+
animation: options,
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
setPaywallData(paywall);
|
|
275
|
+
|
|
276
|
+
if (flow?.currentFlowStep) {
|
|
277
|
+
flow.executeLifecycle(
|
|
278
|
+
flow.currentFlowStep,
|
|
279
|
+
NamiReservedActions.APPEAR,
|
|
280
|
+
);
|
|
281
|
+
lastAppearStepIdRef.current = flow.currentFlowStep.id;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
if (!shouldUseAnimatedTransition) {
|
|
286
|
+
setPendingTransitionPaywall(null);
|
|
287
|
+
applyNavigation();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
setPendingTransitionPaywall(paywall);
|
|
292
|
+
if (pendingNavigationFrameRef.current != null) {
|
|
293
|
+
cancelAnimationFrame(pendingNavigationFrameRef.current);
|
|
294
|
+
}
|
|
295
|
+
pendingNavigationFrameRef.current = requestAnimationFrame(() => {
|
|
296
|
+
pendingNavigationFrameRef.current = null;
|
|
297
|
+
applyNavigation();
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
[flowState.index, flowState.paywalls, paywallData],
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const handleFlowAnimationSettled = useCallback((paywall: IPaywall) => {
|
|
304
|
+
setPendingTransitionPaywall(null);
|
|
305
|
+
setFlowState((prev) => {
|
|
306
|
+
const current = prev.paywalls[prev.index] ?? prev.paywalls[prev.paywalls.length - 1];
|
|
307
|
+
if (!prev.animation && current?.id === paywall.id) {
|
|
308
|
+
return prev;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (prev.paywalls.length > 1 && prev.index > 0) {
|
|
312
|
+
return {
|
|
313
|
+
paywalls: prev.paywalls,
|
|
314
|
+
index: prev.index,
|
|
315
|
+
animation: null,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { paywalls: [paywall], index: 0, animation: null };
|
|
320
|
+
});
|
|
321
|
+
}, []);
|
|
322
|
+
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
|
|
325
|
+
const flow = flowRef.current;
|
|
326
|
+
if (flow?.currentFlowStep) {
|
|
327
|
+
const stepcrumbs = Array.isArray(flow.stepcrumbs) ? flow.stepcrumbs : [];
|
|
328
|
+
const previousScreenStep = [...stepcrumbs]
|
|
329
|
+
.slice(0, -1)
|
|
330
|
+
.reverse()
|
|
331
|
+
.find((step) => step?.type === 'screen');
|
|
332
|
+
const hasRemoteBackActions = !!flow.currentFlowStep?.actions?.[NamiReservedActions.REMOTE_BACK]?.length;
|
|
333
|
+
const canNavigateBackInFlow =
|
|
334
|
+
flow.previousStepAvailable
|
|
335
|
+
|| (
|
|
336
|
+
Boolean(previousScreenStep)
|
|
337
|
+
&& previousScreenStep?.allow_back_to !== false
|
|
338
|
+
);
|
|
339
|
+
if (hasRemoteBackActions) {
|
|
340
|
+
if (!canNavigateBackInFlow) {
|
|
341
|
+
scheduleRemoteBackFallbackClose(
|
|
342
|
+
flow,
|
|
343
|
+
flow.currentFlowStep.id,
|
|
344
|
+
paywallData?.id,
|
|
345
|
+
);
|
|
346
|
+
} else {
|
|
347
|
+
clearRemoteBackFallback();
|
|
348
|
+
}
|
|
349
|
+
flow.triggerActions(NamiReservedActions.REMOTE_BACK);
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
clearRemoteBackFallback();
|
|
353
|
+
if (!canNavigateBackInFlow) {
|
|
354
|
+
finishActiveFlow();
|
|
355
|
+
requestClose();
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
flow.back();
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (paywallData) {
|
|
363
|
+
requestClose();
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return () => subscription.remove();
|
|
370
|
+
}, [clearRemoteBackFallback, finishActiveFlow, paywallData, requestClose, scheduleRemoteBackFallbackClose]);
|
|
371
|
+
|
|
372
|
+
// ─── Paywall action event listener ───────────────────────────────────
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
const emitter = NamiEventEmitter.getInstance();
|
|
375
|
+
|
|
376
|
+
const handler = (event: NamiPaywallEvent) => {
|
|
377
|
+
// Forward to catch-all
|
|
378
|
+
onPaywallEventRef.current?.(event);
|
|
379
|
+
|
|
380
|
+
// Route to specific callbacks
|
|
381
|
+
switch (event.action) {
|
|
382
|
+
case NamiPaywallAction.CLOSE_PAYWALL:
|
|
383
|
+
onCloseRef.current?.();
|
|
384
|
+
break;
|
|
385
|
+
case NamiPaywallAction.SIGN_IN:
|
|
386
|
+
onSignInRef.current?.();
|
|
387
|
+
break;
|
|
388
|
+
case NamiPaywallAction.DEEPLINK:
|
|
389
|
+
if (event.deeplinkUrl) {
|
|
390
|
+
onDeepLinkRef.current?.(event.deeplinkUrl);
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
case NamiPaywallAction.BUY_SKU:
|
|
394
|
+
onPurchaseRef.current?.(event.sku);
|
|
395
|
+
break;
|
|
396
|
+
case NamiPaywallAction.RESTORE_PURCHASES:
|
|
397
|
+
onRestoreRef.current?.();
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
emitter.addListener(PAYWALL_ACTION_EVENT, handler);
|
|
403
|
+
return () => {
|
|
404
|
+
emitter.removeListener(PAYWALL_ACTION_EVENT, handler);
|
|
405
|
+
};
|
|
406
|
+
}, [finishActiveFlow]);
|
|
407
|
+
|
|
408
|
+
// ─── Flow handlers ───────────────────────────────────────────────────
|
|
409
|
+
useEffect(() => {
|
|
410
|
+
NamiFlowManager.registerStepHandoff(
|
|
411
|
+
(handoffTag: string, handoffData?: Record<string, any>) => {
|
|
412
|
+
onHandoffRef.current?.(handoffTag, handoffData);
|
|
413
|
+
},
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
NamiFlowManager.registerEventHandler((eventData: Record<string, any>) => {
|
|
417
|
+
onFlowEventRef.current?.(eventData);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
return () => {
|
|
421
|
+
NamiFlowManager.registerStepHandoff(undefined);
|
|
422
|
+
NamiFlowManager.registerEventHandler(() => {});
|
|
423
|
+
};
|
|
424
|
+
}, []);
|
|
425
|
+
|
|
426
|
+
// ─── ExpoUIAdapter listener subscriptions ────────────────────────────
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
const unsubPaywall = expoUIAdapter.onPaywallRequested((info: any, context) => {
|
|
429
|
+
setLaunchRequest({ type: info?.type, value: info?.value ?? '', context });
|
|
430
|
+
});
|
|
431
|
+
const unsubReRender = expoUIAdapter.onReRenderRequested(() => {
|
|
432
|
+
setReloadKey(k => k + 1);
|
|
433
|
+
});
|
|
434
|
+
const unsubFlow = expoUIAdapter.onFlowNavigationRequested((paywall, options) => {
|
|
435
|
+
handleFlowNavigation(paywall, options);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
return () => {
|
|
439
|
+
unsubPaywall();
|
|
440
|
+
unsubReRender();
|
|
441
|
+
unsubFlow();
|
|
442
|
+
};
|
|
443
|
+
}, [handleFlowNavigation]);
|
|
444
|
+
|
|
445
|
+
useEffect(() => {
|
|
446
|
+
return () => {
|
|
447
|
+
if (pendingNavigationFrameRef.current != null) {
|
|
448
|
+
cancelAnimationFrame(pendingNavigationFrameRef.current);
|
|
449
|
+
pendingNavigationFrameRef.current = null;
|
|
450
|
+
}
|
|
451
|
+
clearRemoteBackFallback();
|
|
452
|
+
finishActiveFlow();
|
|
453
|
+
};
|
|
454
|
+
}, [clearRemoteBackFallback, finishActiveFlow]);
|
|
455
|
+
|
|
456
|
+
// ─── Reset state when launch key changes ─────────────────────────────
|
|
457
|
+
useEffect(() => {
|
|
458
|
+
const launchKey = `${resolvedLaunch.type ?? ''}:${resolvedLaunch.value ?? ''}`;
|
|
459
|
+
if (launchKey !== lastLaunchKeyRef.current) {
|
|
460
|
+
lastLaunchKeyRef.current = launchKey;
|
|
461
|
+
setIsClosing(false);
|
|
462
|
+
setFlowState({ paywalls: [], index: 0, animation: null });
|
|
463
|
+
finishActiveFlow();
|
|
464
|
+
setPaywallData(null);
|
|
465
|
+
setCampaignData(null);
|
|
466
|
+
setLaunchPreviewPaywall(null);
|
|
467
|
+
setPendingTransitionPaywall(null);
|
|
468
|
+
setInitialScreenCommitted(false);
|
|
469
|
+
lastAppearStepIdRef.current = '';
|
|
470
|
+
}
|
|
471
|
+
}, [finishActiveFlow, resolvedLaunch.type, resolvedLaunch.value]);
|
|
472
|
+
|
|
473
|
+
// ─── Campaign / paywall resolution ───────────────────────────────────
|
|
474
|
+
useEffect(() => {
|
|
475
|
+
let cancelled = false;
|
|
476
|
+
|
|
477
|
+
const resolveLaunch = async () => {
|
|
478
|
+
const value = resolvedLaunch.value;
|
|
479
|
+
const type = resolvedLaunch.type ?? (url ? 'url' : 'label');
|
|
480
|
+
const cacheKey = `${type}:${value}`;
|
|
481
|
+
if (!value) {
|
|
482
|
+
setLoading(false);
|
|
483
|
+
setPaywallData(null);
|
|
484
|
+
setCampaignData(null);
|
|
485
|
+
setLaunchPreviewPaywall(null);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const cachedLaunch = launchCacheRef.current.get(cacheKey);
|
|
490
|
+
if (cachedLaunch) {
|
|
491
|
+
setLaunchPreviewPaywall(cachedLaunch.launchPreviewPaywall);
|
|
492
|
+
setPaywallData(cachedLaunch.paywallData);
|
|
493
|
+
setCampaignData(cachedLaunch.campaignData);
|
|
494
|
+
setLoading(false);
|
|
495
|
+
} else {
|
|
496
|
+
setLoading(true);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
const data = getPaywallDataFromLabel(value, type);
|
|
501
|
+
const paywall = data?.paywall as IPaywall | undefined;
|
|
502
|
+
const flowPaywalls = isNamiFlowCampaign(data?.campaign as any)
|
|
503
|
+
? (data?.campaign?.flow?.object?.screens ?? [])
|
|
504
|
+
.map((screenId: string) => getPaywall(screenId))
|
|
505
|
+
.filter((item): item is IPaywall => item != null)
|
|
506
|
+
: [];
|
|
507
|
+
const initialPaywall = paywall ?? flowPaywalls[0];
|
|
508
|
+
const nextCampaign = data?.campaign ? (data.campaign as NamiCampaign) : ({ value, type } as any);
|
|
509
|
+
const nextLaunch = {
|
|
510
|
+
launchPreviewPaywall: initialPaywall ?? null,
|
|
511
|
+
paywallData: data?.paywall ?? null,
|
|
512
|
+
campaignData: nextCampaign,
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
if (!cancelled) {
|
|
516
|
+
setLaunchPreviewPaywall(nextLaunch.launchPreviewPaywall);
|
|
517
|
+
setInitialScreenCommitted(false);
|
|
518
|
+
setPaywallData(nextLaunch.paywallData);
|
|
519
|
+
setCampaignData(nextLaunch.campaignData);
|
|
520
|
+
setLoading(false);
|
|
521
|
+
launchCacheRef.current.set(cacheKey, nextLaunch);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
prewarmPaywallFonts([paywall, ...flowPaywalls]);
|
|
525
|
+
|
|
526
|
+
if (initialPaywall?.fonts) {
|
|
527
|
+
void prepareAndLoadFontsWithTimeout(initialPaywall.fonts);
|
|
528
|
+
}
|
|
529
|
+
} catch (e) {
|
|
530
|
+
if (!cancelled) {
|
|
531
|
+
setLoading(false);
|
|
532
|
+
}
|
|
533
|
+
logger.warn('[NamiExpo] Failed to resolve paywall launch.', e);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
void resolveLaunch();
|
|
538
|
+
|
|
539
|
+
return () => {
|
|
540
|
+
cancelled = true;
|
|
541
|
+
};
|
|
542
|
+
}, [resolvedLaunch.value, resolvedLaunch.type, url, reloadKey]);
|
|
543
|
+
|
|
544
|
+
// ─── Flow presentation ───────────────────────────────────────────────
|
|
545
|
+
useEffect(() => {
|
|
546
|
+
if (!campaignData) return;
|
|
547
|
+
if (!isNamiFlowCampaign(campaignData as any)) return;
|
|
548
|
+
|
|
549
|
+
const screens = campaignData.flow?.object?.screens ?? [];
|
|
550
|
+
if (screens.length && !hasAllPaywalls(screens)) return;
|
|
551
|
+
|
|
552
|
+
if (flowRef.current) return;
|
|
553
|
+
|
|
554
|
+
const flow = NamiFlowManager.instance.presentFlow(campaignData as any, {
|
|
555
|
+
type: resolvedLaunch.type,
|
|
556
|
+
value: resolvedLaunch.value,
|
|
557
|
+
context: ctx,
|
|
558
|
+
} as any, ctx);
|
|
559
|
+
|
|
560
|
+
flowRef.current = flow;
|
|
561
|
+
}, [campaignData, resolvedLaunch.type, resolvedLaunch.value, ctx, reloadKey]);
|
|
562
|
+
|
|
563
|
+
useEffect(() => {
|
|
564
|
+
const flow = flowRef.current;
|
|
565
|
+
const activePaywall =
|
|
566
|
+
flowState.paywalls[flowState.index] ??
|
|
567
|
+
flowState.paywalls[flowState.paywalls.length - 1];
|
|
568
|
+
|
|
569
|
+
if (!flow || !activePaywall || !flow.currentFlowStep) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const currentStep = flow.currentFlowStep;
|
|
574
|
+
if (lastAppearStepIdRef.current === currentStep.id) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
flow.executeLifecycle(currentStep, NamiReservedActions.APPEAR);
|
|
579
|
+
lastAppearStepIdRef.current = currentStep.id;
|
|
580
|
+
}, [flowState.paywalls, flowState.index]);
|
|
581
|
+
|
|
582
|
+
useEffect(() => {
|
|
583
|
+
const attempt = remoteBackFallbackAttemptRef.current;
|
|
584
|
+
if (!attempt) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (isClosing) {
|
|
589
|
+
clearRemoteBackFallback();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const activeFlow = flowRef.current;
|
|
594
|
+
const activeStepId = activeFlow?.currentFlowStep?.id;
|
|
595
|
+
const activePaywallId = paywallData?.id;
|
|
596
|
+
const didNavigateAway =
|
|
597
|
+
attempt.flow !== activeFlow
|
|
598
|
+
|| attempt.stepId !== activeStepId
|
|
599
|
+
|| attempt.paywallId !== activePaywallId;
|
|
600
|
+
|
|
601
|
+
if (didNavigateAway) {
|
|
602
|
+
clearRemoteBackFallback();
|
|
603
|
+
}
|
|
604
|
+
}, [clearRemoteBackFallback, flowState.index, flowState.paywalls, isClosing, paywallData?.id]);
|
|
605
|
+
|
|
606
|
+
const hasFlowScreen = flowState.paywalls.length > 0;
|
|
607
|
+
const launchPlaceholderPaywall =
|
|
608
|
+
launchPreviewPaywall
|
|
609
|
+
?? paywallData
|
|
610
|
+
?? flowState.paywalls[flowState.index]
|
|
611
|
+
?? flowState.paywalls[0]
|
|
612
|
+
?? null;
|
|
613
|
+
const showLaunchPlaceholder =
|
|
614
|
+
Boolean(launchPlaceholderPaywall)
|
|
615
|
+
&& (loading || !initialScreenCommitted);
|
|
616
|
+
const showTransitionPlaceholder =
|
|
617
|
+
Boolean(pendingTransitionPaywall)
|
|
618
|
+
&& !loading
|
|
619
|
+
&& !isClosing;
|
|
620
|
+
|
|
621
|
+
const handleInitialCommitted = useCallback((_paywall: IPaywall, _pageName: string, _formFactor?: string) => {
|
|
622
|
+
setInitialScreenCommitted(true);
|
|
623
|
+
}, []);
|
|
624
|
+
|
|
625
|
+
if (isClosing) {
|
|
626
|
+
return <SafeAreaView style={styles.root} />;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (loading || !campaignData || (!isFlowCampaign && !paywallData) || (isFlowCampaign && !hasFlowScreen)) {
|
|
630
|
+
return <LaunchPlaceholder paywall={launchPlaceholderPaywall} />;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return (
|
|
634
|
+
<SafeAreaView style={styles.root}>
|
|
635
|
+
<StatusBar barStyle="light-content" />
|
|
636
|
+
{isFlowCampaign ? (
|
|
637
|
+
<FlowRenderer
|
|
638
|
+
paywalls={flowState.paywalls}
|
|
639
|
+
currentIndex={flowState.index}
|
|
640
|
+
animation={flowState.animation ?? undefined}
|
|
641
|
+
onSettled={handleFlowAnimationSettled}
|
|
642
|
+
onTransitionVisible={() => setPendingTransitionPaywall(null)}
|
|
643
|
+
onInitialCommitted={handleInitialCommitted}
|
|
644
|
+
onClose={onClose}
|
|
645
|
+
campaign={campaignData}
|
|
646
|
+
context={ctx}
|
|
647
|
+
flow={flowRef.current}
|
|
648
|
+
/>
|
|
649
|
+
) : (
|
|
650
|
+
<PaywallProvider paywall={paywallData as IPaywall} context={ctx} campaign={campaignData}>
|
|
651
|
+
<PaywallScreen
|
|
652
|
+
paywall={paywallData as IPaywall}
|
|
653
|
+
onClose={requestClose}
|
|
654
|
+
onCommitted={handleInitialCommitted}
|
|
655
|
+
isActive
|
|
656
|
+
/>
|
|
657
|
+
</PaywallProvider>
|
|
658
|
+
)}
|
|
659
|
+
{!isClosing && showLaunchPlaceholder && <LaunchPlaceholder paywall={launchPlaceholderPaywall} overlay />}
|
|
660
|
+
{showTransitionPlaceholder && <LaunchPlaceholder paywall={pendingTransitionPaywall} overlay />}
|
|
661
|
+
</SafeAreaView>
|
|
662
|
+
);
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const LaunchPlaceholder: React.FC<{
|
|
666
|
+
paywall: IPaywall | null;
|
|
667
|
+
overlay?: boolean;
|
|
668
|
+
}> = ({ paywall, overlay = false }) => {
|
|
669
|
+
const initialPageName = paywall?.template?.initialState?.currentPage ?? 'page1';
|
|
670
|
+
const page = paywall?.template?.pages?.find((item: any) => item?.name === initialPageName) ?? null;
|
|
671
|
+
const backgroundContainer = (page as any)?.backgroundContainer;
|
|
672
|
+
const backgroundColor =
|
|
673
|
+
parseColor(backgroundContainer?.fillColor)
|
|
674
|
+
?? parseColor(backgroundContainer?.fillColorFallback)
|
|
675
|
+
?? '#111118';
|
|
676
|
+
|
|
677
|
+
return (
|
|
678
|
+
<View
|
|
679
|
+
pointerEvents="none"
|
|
680
|
+
style={[
|
|
681
|
+
styles.loading,
|
|
682
|
+
overlay ? styles.loadingOverlay : null,
|
|
683
|
+
{ backgroundColor },
|
|
684
|
+
]}
|
|
685
|
+
>
|
|
686
|
+
<ActivityIndicator size="large" color="#FFFFFF" />
|
|
687
|
+
</View>
|
|
688
|
+
);
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const getPaywallBackgroundColor = (paywall: IPaywall | null | undefined): string => {
|
|
692
|
+
const initialPageName = paywall?.template?.initialState?.currentPage ?? 'page1';
|
|
693
|
+
const page = paywall?.template?.pages?.find((item: any) => item?.name === initialPageName) ?? null;
|
|
694
|
+
const backgroundContainer = (page as any)?.backgroundContainer;
|
|
695
|
+
|
|
696
|
+
return (
|
|
697
|
+
parseColor(backgroundContainer?.fillColor)
|
|
698
|
+
?? parseColor(backgroundContainer?.fillColorFallback)
|
|
699
|
+
?? '#111118'
|
|
700
|
+
);
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
/** Renders multi-step flow paywalls with transition animations */
|
|
704
|
+
const FlowRenderer: React.FC<{
|
|
705
|
+
paywalls: IPaywall[];
|
|
706
|
+
currentIndex: number;
|
|
707
|
+
animation?: FlowNavigationOptions;
|
|
708
|
+
onSettled?: (paywall: IPaywall) => void;
|
|
709
|
+
onTransitionVisible?: () => void;
|
|
710
|
+
onInitialCommitted?: (paywall: IPaywall, pageName: string, formFactor?: string) => void;
|
|
711
|
+
onClose?: () => void;
|
|
712
|
+
campaign: NamiCampaign;
|
|
713
|
+
context: NamiPaywallLaunchContext;
|
|
714
|
+
flow?: NamiFlow;
|
|
715
|
+
}> = ({ paywalls, currentIndex, animation, onSettled, onTransitionVisible, onInitialCommitted, onClose, campaign, context, flow }) => {
|
|
716
|
+
const { width, height } = Dimensions.get('window');
|
|
717
|
+
const progress = useRef(new Animated.Value(0)).current;
|
|
718
|
+
const transitionSequenceRef = useRef(0);
|
|
719
|
+
const lastTransitionSignatureRef = useRef('');
|
|
720
|
+
const startedTransitionKeyRef = useRef<string | null>(null);
|
|
721
|
+
const [completedTransitionKey, setCompletedTransitionKey] = useState<string | null>(null);
|
|
722
|
+
const transition = animation?.transition ?? 'slide';
|
|
723
|
+
const direction = animation?.direction === 'backward' ? -1 : 1;
|
|
724
|
+
const isVertical = transition === 'verticalSlide';
|
|
725
|
+
const distance = isVertical ? height : width;
|
|
726
|
+
const shouldAnimatePair =
|
|
727
|
+
paywalls.length === 2
|
|
728
|
+
&& currentIndex === 1
|
|
729
|
+
&& animation != null;
|
|
730
|
+
const shouldRenderPair = shouldAnimatePair;
|
|
731
|
+
const sourcePaywall = shouldRenderPair ? paywalls[0] : undefined;
|
|
732
|
+
const targetPaywall = shouldRenderPair
|
|
733
|
+
? paywalls[1]
|
|
734
|
+
: (paywalls[currentIndex] ?? paywalls[0]);
|
|
735
|
+
const targetBackgroundColor = useMemo(
|
|
736
|
+
() => getPaywallBackgroundColor(targetPaywall),
|
|
737
|
+
[targetPaywall],
|
|
738
|
+
);
|
|
739
|
+
const transitionSignature = shouldAnimatePair
|
|
740
|
+
? `${sourcePaywall?.id ?? 'source'}->${targetPaywall?.id ?? 'target'}:${transition}:${direction}`
|
|
741
|
+
: '';
|
|
742
|
+
|
|
743
|
+
if (shouldAnimatePair && lastTransitionSignatureRef.current !== transitionSignature) {
|
|
744
|
+
lastTransitionSignatureRef.current = transitionSignature;
|
|
745
|
+
transitionSequenceRef.current += 1;
|
|
746
|
+
} else if (!shouldAnimatePair) {
|
|
747
|
+
lastTransitionSignatureRef.current = '';
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const activeTransitionKey = shouldAnimatePair
|
|
751
|
+
? `${transitionSignature}:${transitionSequenceRef.current}`
|
|
752
|
+
: '';
|
|
753
|
+
const [committedTarget, setCommittedTarget] = useState<{
|
|
754
|
+
transitionKey: string | null;
|
|
755
|
+
paywallId: string | null;
|
|
756
|
+
}>({
|
|
757
|
+
transitionKey: null,
|
|
758
|
+
paywallId: null,
|
|
759
|
+
});
|
|
760
|
+
const interactionReady =
|
|
761
|
+
!shouldAnimatePair
|
|
762
|
+
|| (
|
|
763
|
+
committedTarget.transitionKey === activeTransitionKey
|
|
764
|
+
&& committedTarget.paywallId === (targetPaywall?.id ?? null)
|
|
765
|
+
);
|
|
766
|
+
const transitionReady = shouldAnimatePair && interactionReady;
|
|
767
|
+
const animationReady =
|
|
768
|
+
!shouldAnimatePair
|
|
769
|
+
|| transitionReady;
|
|
770
|
+
|
|
771
|
+
const handleTargetCommitted = useCallback((paywall: IPaywall, _pageName: string, formFactor?: string) => {
|
|
772
|
+
if (paywall.id !== targetPaywall?.id) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
setCommittedTarget({
|
|
776
|
+
transitionKey: activeTransitionKey || null,
|
|
777
|
+
paywallId: paywall.id ?? null,
|
|
778
|
+
});
|
|
779
|
+
}, [activeTransitionKey, targetPaywall?.id]);
|
|
780
|
+
|
|
781
|
+
const handleVisibleCommitted = useCallback((paywall: IPaywall, pageName: string, formFactor?: string) => {
|
|
782
|
+
if (currentIndex === 0) {
|
|
783
|
+
onInitialCommitted?.(paywall, pageName, formFactor);
|
|
784
|
+
}
|
|
785
|
+
handleTargetCommitted(paywall, pageName, formFactor);
|
|
786
|
+
}, [currentIndex, handleTargetCommitted, onInitialCommitted]);
|
|
787
|
+
|
|
788
|
+
useLayoutEffect(() => {
|
|
789
|
+
const activePaywall = paywalls[currentIndex] ?? paywalls[paywalls.length - 1];
|
|
790
|
+
|
|
791
|
+
if (!shouldAnimatePair) {
|
|
792
|
+
startedTransitionKeyRef.current = null;
|
|
793
|
+
setCompletedTransitionKey(null);
|
|
794
|
+
progress.stopAnimation();
|
|
795
|
+
progress.setValue(0);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (transition === 'fade' || transition === 'none') {
|
|
800
|
+
if (startedTransitionKeyRef.current === activeTransitionKey) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
startedTransitionKeyRef.current = activeTransitionKey;
|
|
805
|
+
setCompletedTransitionKey(null);
|
|
806
|
+
progress.stopAnimation();
|
|
807
|
+
progress.setValue(0);
|
|
808
|
+
if (activePaywall) {
|
|
809
|
+
requestAnimationFrame(() => {
|
|
810
|
+
onSettled?.(activePaywall);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (!transitionReady) {
|
|
817
|
+
if (startedTransitionKeyRef.current !== activeTransitionKey) {
|
|
818
|
+
progress.stopAnimation();
|
|
819
|
+
progress.setValue(0);
|
|
820
|
+
}
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (startedTransitionKeyRef.current === activeTransitionKey) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
startedTransitionKeyRef.current = activeTransitionKey;
|
|
829
|
+
setCompletedTransitionKey(null);
|
|
830
|
+
progress.stopAnimation();
|
|
831
|
+
progress.setValue(0);
|
|
832
|
+
Animated.timing(progress, {
|
|
833
|
+
toValue: 1,
|
|
834
|
+
duration: 250,
|
|
835
|
+
useNativeDriver: true,
|
|
836
|
+
}).start(({ finished }) => {
|
|
837
|
+
if (finished) {
|
|
838
|
+
setCompletedTransitionKey(activeTransitionKey);
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
}, [
|
|
842
|
+
activeTransitionKey,
|
|
843
|
+
animation?.direction,
|
|
844
|
+
currentIndex,
|
|
845
|
+
onSettled,
|
|
846
|
+
paywalls,
|
|
847
|
+
progress,
|
|
848
|
+
sourcePaywall?.id,
|
|
849
|
+
transitionReady,
|
|
850
|
+
transition,
|
|
851
|
+
]);
|
|
852
|
+
|
|
853
|
+
useEffect(() => {
|
|
854
|
+
if (
|
|
855
|
+
!shouldAnimatePair
|
|
856
|
+
|| !targetPaywall
|
|
857
|
+
|| !interactionReady
|
|
858
|
+
|| completedTransitionKey !== activeTransitionKey
|
|
859
|
+
) {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
onSettled?.(targetPaywall);
|
|
864
|
+
}, [
|
|
865
|
+
activeTransitionKey,
|
|
866
|
+
completedTransitionKey,
|
|
867
|
+
interactionReady,
|
|
868
|
+
onSettled,
|
|
869
|
+
shouldAnimatePair,
|
|
870
|
+
targetPaywall,
|
|
871
|
+
]);
|
|
872
|
+
|
|
873
|
+
useEffect(() => {
|
|
874
|
+
if (!shouldAnimatePair || !interactionReady) {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
onTransitionVisible?.();
|
|
879
|
+
}, [interactionReady, onTransitionVisible, shouldAnimatePair]);
|
|
880
|
+
|
|
881
|
+
if (transition === 'fade') {
|
|
882
|
+
const current = paywalls[currentIndex] ?? paywalls[0];
|
|
883
|
+
if (!current) return null;
|
|
884
|
+
return (
|
|
885
|
+
<View style={styles.flowContainer}>
|
|
886
|
+
<View style={{ width, height }}>
|
|
887
|
+
<FocusProvider enabled>
|
|
888
|
+
<PaywallProvider paywall={current} context={context} campaign={campaign} flow={flow}>
|
|
889
|
+
<PaywallScreen
|
|
890
|
+
paywall={current}
|
|
891
|
+
onClose={onClose}
|
|
892
|
+
onCommitted={handleVisibleCommitted}
|
|
893
|
+
isActive
|
|
894
|
+
/>
|
|
895
|
+
</PaywallProvider>
|
|
896
|
+
</FocusProvider>
|
|
897
|
+
</View>
|
|
898
|
+
</View>
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const renderedPaywalls = shouldRenderPair
|
|
903
|
+
? [
|
|
904
|
+
{ paywall: sourcePaywall, role: 'source' as const, key: 0 },
|
|
905
|
+
{ paywall: targetPaywall, role: 'target' as const, key: 1 },
|
|
906
|
+
]
|
|
907
|
+
: [{ paywall: paywalls[currentIndex] ?? paywalls[0], role: 'target' as const, key: currentIndex }];
|
|
908
|
+
|
|
909
|
+
return (
|
|
910
|
+
<View style={styles.flowContainer}>
|
|
911
|
+
{renderedPaywalls.map(({ paywall: pw, role, key }) => {
|
|
912
|
+
if (!pw) {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const animatedStyle = role === 'target' && shouldAnimatePair
|
|
917
|
+
? { opacity: 0 }
|
|
918
|
+
: null;
|
|
919
|
+
|
|
920
|
+
return (
|
|
921
|
+
<Animated.View
|
|
922
|
+
key={pw.id ?? key}
|
|
923
|
+
pointerEvents={role === 'target' && !shouldAnimatePair && interactionReady ? 'auto' : 'none'}
|
|
924
|
+
style={[
|
|
925
|
+
styles.flowScreen,
|
|
926
|
+
{
|
|
927
|
+
width,
|
|
928
|
+
height,
|
|
929
|
+
zIndex: role === 'target' ? 2 : 1,
|
|
930
|
+
elevation: role === 'target' ? 2 : 1,
|
|
931
|
+
},
|
|
932
|
+
animatedStyle ?? null,
|
|
933
|
+
]}
|
|
934
|
+
>
|
|
935
|
+
<FocusProvider enabled={role === 'target' && !shouldAnimatePair && interactionReady}>
|
|
936
|
+
<PaywallProvider
|
|
937
|
+
paywall={pw}
|
|
938
|
+
context={context}
|
|
939
|
+
campaign={campaign}
|
|
940
|
+
flow={flow}
|
|
941
|
+
>
|
|
942
|
+
<PaywallScreen
|
|
943
|
+
paywall={pw}
|
|
944
|
+
onClose={onClose}
|
|
945
|
+
onCommitted={role === 'target' ? handleVisibleCommitted : undefined}
|
|
946
|
+
isActive={role === 'target' && !shouldAnimatePair && interactionReady}
|
|
947
|
+
/>
|
|
948
|
+
</PaywallProvider>
|
|
949
|
+
</FocusProvider>
|
|
950
|
+
</Animated.View>
|
|
951
|
+
);
|
|
952
|
+
})}
|
|
953
|
+
{shouldAnimatePair && !interactionReady && (
|
|
954
|
+
<LaunchPlaceholder paywall={targetPaywall ?? null} overlay />
|
|
955
|
+
)}
|
|
956
|
+
{transitionReady && targetPaywall && (
|
|
957
|
+
<Animated.View
|
|
958
|
+
pointerEvents="none"
|
|
959
|
+
style={[
|
|
960
|
+
styles.flowScreen,
|
|
961
|
+
{
|
|
962
|
+
width,
|
|
963
|
+
height,
|
|
964
|
+
zIndex: 3,
|
|
965
|
+
elevation: 3,
|
|
966
|
+
backgroundColor: targetBackgroundColor,
|
|
967
|
+
},
|
|
968
|
+
isVertical
|
|
969
|
+
? {
|
|
970
|
+
transform: [{
|
|
971
|
+
translateY: progress.interpolate({
|
|
972
|
+
inputRange: [0, 1],
|
|
973
|
+
outputRange: [height * direction, 0],
|
|
974
|
+
}),
|
|
975
|
+
}],
|
|
976
|
+
}
|
|
977
|
+
: {
|
|
978
|
+
transform: [{
|
|
979
|
+
translateX: progress.interpolate({
|
|
980
|
+
inputRange: [0, 1],
|
|
981
|
+
outputRange: [width * direction, 0],
|
|
982
|
+
}),
|
|
983
|
+
}],
|
|
984
|
+
},
|
|
985
|
+
]}
|
|
986
|
+
/>
|
|
987
|
+
)}
|
|
988
|
+
</View>
|
|
989
|
+
);
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
const styles = StyleSheet.create({
|
|
993
|
+
root: { flex: 1, backgroundColor: '#000' },
|
|
994
|
+
loading: {
|
|
995
|
+
flex: 1,
|
|
996
|
+
backgroundColor: '#111118',
|
|
997
|
+
alignItems: 'center',
|
|
998
|
+
justifyContent: 'center',
|
|
999
|
+
},
|
|
1000
|
+
loadingOverlay: {
|
|
1001
|
+
...StyleSheet.absoluteFillObject,
|
|
1002
|
+
zIndex: 50,
|
|
1003
|
+
},
|
|
1004
|
+
flowContainer: { flex: 1, overflow: 'hidden' },
|
|
1005
|
+
flowScreen: { position: 'absolute', top: 0, left: 0 },
|
|
1006
|
+
});
|