@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,245 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { ActivityIndicator, BackHandler, View, StyleSheet } from 'react-native';
|
|
3
|
+
import type { IPaywall, TPages } from '@namiml/sdk-core';
|
|
4
|
+
import { useFirstFocusReadyContext, usePaywallContext } from '../context/PaywallContext';
|
|
5
|
+
import { NamiContentContainer } from './containers/NamiContentContainer';
|
|
6
|
+
import { NamiBackgroundContainer } from './containers/NamiBackgroundContainer';
|
|
7
|
+
import { NamiHeader } from './containers/NamiHeader';
|
|
8
|
+
import { NamiFooter } from './containers/NamiFooter';
|
|
9
|
+
import { parseColor } from '../utils/styles';
|
|
10
|
+
import { getDeviceScaleFactor, NamiEventEmitter, NamiPaywallAction, PAYWALL_ACTION_EVENT, NamiReservedActions } from '@namiml/sdk-core';
|
|
11
|
+
import { prepareAndLoadFontsWithTimeout } from '../utils/fonts';
|
|
12
|
+
import { postImpression } from '../utils/impression';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
paywall: IPaywall;
|
|
16
|
+
onClose?: () => void;
|
|
17
|
+
onCommitted?: (paywall: IPaywall, pageName: string, formFactor?: string) => void;
|
|
18
|
+
holdInteractionUntilFocus?: boolean;
|
|
19
|
+
isActive?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let KeplerTVFocusGuideView: React.ComponentType<any> | null = null;
|
|
23
|
+
try {
|
|
24
|
+
KeplerTVFocusGuideView = require('@amazon-devices/react-native-kepler').TVFocusGuideView;
|
|
25
|
+
} catch {
|
|
26
|
+
KeplerTVFocusGuideView = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fontGateCache = new Map<string, 'ready' | 'fallback'>();
|
|
30
|
+
|
|
31
|
+
export const PaywallScreen: React.FC<Props> = ({
|
|
32
|
+
paywall,
|
|
33
|
+
onClose,
|
|
34
|
+
onCommitted,
|
|
35
|
+
holdInteractionUntilFocus = false,
|
|
36
|
+
isActive = true,
|
|
37
|
+
}) => {
|
|
38
|
+
const ctx = usePaywallContext();
|
|
39
|
+
const focusReadyCtx = useFirstFocusReadyContext();
|
|
40
|
+
const scaleFactor = getDeviceScaleFactor(ctx.state.formFactor);
|
|
41
|
+
const userInteractionEnabled = ctx.state.userInteractionEnabled !== false;
|
|
42
|
+
const currentPageName = ctx.state.selectedPaywall === paywall
|
|
43
|
+
? ctx.state.currentPage
|
|
44
|
+
: paywall.template?.initialState?.currentPage ?? 'page1';
|
|
45
|
+
|
|
46
|
+
const page = useMemo(() => {
|
|
47
|
+
let currentPage: string;
|
|
48
|
+
if (ctx.state.selectedPaywall === paywall) {
|
|
49
|
+
currentPage = ctx.state.currentPage;
|
|
50
|
+
} else {
|
|
51
|
+
currentPage = paywall.template?.initialState?.currentPage ?? 'page1';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return paywall.template?.pages?.find((p: TPages) => p.name === currentPage) ?? null;
|
|
55
|
+
}, [paywall, ctx.state.selectedPaywall, ctx.state.currentPage]);
|
|
56
|
+
|
|
57
|
+
const firstFocusReadyKey = `${paywall.id ?? 'unknown'}:${page?.name ?? currentPageName}:${ctx.state.formFactor ?? ''}`;
|
|
58
|
+
const shouldHoldForFocus = holdInteractionUntilFocus && ctx.state.formFactor === 'television';
|
|
59
|
+
const focusReady = !shouldHoldForFocus || focusReadyCtx.firstFocusReadyKey === firstFocusReadyKey;
|
|
60
|
+
const interactionEnabled = userInteractionEnabled && focusReady;
|
|
61
|
+
const fontNames = useMemo(() => Object.keys(paywall.fonts ?? {}).sort(), [paywall.fonts]);
|
|
62
|
+
const fontGateCacheKey = `${paywall.id ?? 'unknown'}:${page?.name ?? currentPageName}:${fontNames.join(',')}`;
|
|
63
|
+
const hasFonts = fontNames.length > 0;
|
|
64
|
+
const [fontGateState, setFontGateState] = useState<'pending' | 'ready' | 'fallback'>(() => {
|
|
65
|
+
if (!hasFonts) {
|
|
66
|
+
return 'ready';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return fontGateCache.get(fontGateCacheKey) ?? 'pending';
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
void postImpression({
|
|
74
|
+
segment: ctx.state.selectedCampaign?.segment,
|
|
75
|
+
call_to_action: paywall.id,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
NamiEventEmitter.getInstance().emit(PAYWALL_ACTION_EVENT, {
|
|
79
|
+
...ctx.getPaywallActionEventData(),
|
|
80
|
+
action: NamiPaywallAction.SHOW_PAYWALL,
|
|
81
|
+
});
|
|
82
|
+
}, [paywall.id, ctx.state.selectedCampaign?.segment]);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
let cancelled = false;
|
|
86
|
+
|
|
87
|
+
if (!hasFonts) {
|
|
88
|
+
setFontGateState('ready');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const cachedGateState = fontGateCache.get(fontGateCacheKey);
|
|
93
|
+
if (cachedGateState) {
|
|
94
|
+
setFontGateState(cachedGateState);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setFontGateState('pending');
|
|
99
|
+
|
|
100
|
+
void prepareAndLoadFontsWithTimeout(paywall.fonts, 1500).then((result) => {
|
|
101
|
+
if (!cancelled) {
|
|
102
|
+
const resolvedState = result === 'ready' ? 'ready' : 'fallback';
|
|
103
|
+
fontGateCache.set(fontGateCacheKey, resolvedState);
|
|
104
|
+
setFontGateState(resolvedState);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true;
|
|
110
|
+
};
|
|
111
|
+
}, [currentPageName, fontGateCacheKey, fontNames.length, hasFonts, page?.name, paywall.fonts, paywall.id]);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (fontGateState === 'pending') {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const paywallId = paywall.id ?? 'unknown';
|
|
119
|
+
const pageName = page?.name ?? currentPageName;
|
|
120
|
+
onCommitted?.(paywall, pageName, ctx.state.formFactor);
|
|
121
|
+
}, [fontGateState, paywall, paywall.id, page?.name, currentPageName, interactionEnabled, onCommitted, ctx.state.formFactor]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!isActive) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
129
|
+
const hasRemoteBackActions = !!ctx.flow?.currentFlowStep?.actions?.[NamiReservedActions.REMOTE_BACK]?.length;
|
|
130
|
+
if (hasRemoteBackActions) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const canGoBackPage = ctx.canGoBackPage();
|
|
135
|
+
if (!canGoBackPage) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return ctx.goBackPage();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return () => subscription.remove();
|
|
143
|
+
}, [ctx, isActive]);
|
|
144
|
+
|
|
145
|
+
const {
|
|
146
|
+
backgroundContainer,
|
|
147
|
+
header = [],
|
|
148
|
+
contentContainer,
|
|
149
|
+
footer = [],
|
|
150
|
+
} = (page ?? {}) as any;
|
|
151
|
+
|
|
152
|
+
const hasHeader = Array.isArray(header)
|
|
153
|
+
? header.length > 0
|
|
154
|
+
: Array.isArray((header as any)?.components)
|
|
155
|
+
? (header as any).components.length > 0
|
|
156
|
+
: Boolean(header);
|
|
157
|
+
const hasFooter = Array.isArray(footer)
|
|
158
|
+
? footer.length > 0
|
|
159
|
+
: Array.isArray((footer as any)?.components)
|
|
160
|
+
? (footer as any).components.length > 0
|
|
161
|
+
: Boolean(footer);
|
|
162
|
+
const bgColor = parseColor(backgroundContainer?.fillColor)
|
|
163
|
+
?? parseColor(backgroundContainer?.fillColorFallback)
|
|
164
|
+
?? 'transparent';
|
|
165
|
+
const waitingOverlayColor = bgColor === 'transparent' ? '#111118' : bgColor;
|
|
166
|
+
const PageFocusWrapper = ctx.state.formFactor === 'television' && KeplerTVFocusGuideView
|
|
167
|
+
? KeplerTVFocusGuideView
|
|
168
|
+
: View;
|
|
169
|
+
const pageFocusWrapperProps = ctx.state.formFactor === 'television' && KeplerTVFocusGuideView
|
|
170
|
+
? {
|
|
171
|
+
autoFocus: true,
|
|
172
|
+
style: styles.pageRoot,
|
|
173
|
+
}
|
|
174
|
+
: { style: styles.pageRoot };
|
|
175
|
+
|
|
176
|
+
if (!page) {
|
|
177
|
+
throw new Error(`Page with name ${ctx.state.currentPage} not found in paywall template.`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (fontGateState === 'pending') {
|
|
181
|
+
return (
|
|
182
|
+
<View style={[styles.screen, styles.pendingScreen, { backgroundColor: waitingOverlayColor }]}>
|
|
183
|
+
<ActivityIndicator size="large" color="#FFFFFF" />
|
|
184
|
+
</View>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<View
|
|
190
|
+
style={[styles.screen, { backgroundColor: bgColor }]}
|
|
191
|
+
pointerEvents={interactionEnabled ? 'auto' : 'none'}
|
|
192
|
+
>
|
|
193
|
+
<PageFocusWrapper
|
|
194
|
+
key={`${paywall.id}:${page.name ?? currentPageName}`}
|
|
195
|
+
{...pageFocusWrapperProps}
|
|
196
|
+
>
|
|
197
|
+
{backgroundContainer && (
|
|
198
|
+
<NamiBackgroundContainer component={backgroundContainer as any} scaleFactor={scaleFactor} onClose={onClose} />
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{hasHeader && (
|
|
202
|
+
<NamiHeader
|
|
203
|
+
components={header as any}
|
|
204
|
+
scaleFactor={scaleFactor}
|
|
205
|
+
onClose={onClose}
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
{contentContainer && (
|
|
210
|
+
<NamiContentContainer
|
|
211
|
+
component={contentContainer as any}
|
|
212
|
+
scaleFactor={scaleFactor}
|
|
213
|
+
onClose={onClose}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
{hasFooter && (
|
|
218
|
+
<NamiFooter components={footer as any} scaleFactor={scaleFactor} onClose={onClose} />
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{shouldHoldForFocus && !focusReady && (
|
|
222
|
+
<View
|
|
223
|
+
pointerEvents="none"
|
|
224
|
+
style={[styles.waitingOverlay, { backgroundColor: waitingOverlayColor }]}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
</PageFocusWrapper>
|
|
228
|
+
</View>
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const styles = StyleSheet.create({
|
|
233
|
+
screen: { flex: 1 },
|
|
234
|
+
pendingScreen: {
|
|
235
|
+
alignItems: 'center',
|
|
236
|
+
justifyContent: 'center',
|
|
237
|
+
},
|
|
238
|
+
pageRoot: { flex: 1 },
|
|
239
|
+
waitingOverlay: {
|
|
240
|
+
...StyleSheet.absoluteFillObject,
|
|
241
|
+
alignItems: 'center',
|
|
242
|
+
justifyContent: 'center',
|
|
243
|
+
zIndex: 20,
|
|
244
|
+
},
|
|
245
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { TComponent } from '@namiml/sdk-core';
|
|
3
|
+
import { usePaywallContext } from '../context/PaywallContext';
|
|
4
|
+
import {
|
|
5
|
+
conditionComponentMatches,
|
|
6
|
+
resolveComponentSmartText,
|
|
7
|
+
withOverrides,
|
|
8
|
+
} from '../utils/rendering';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
component: TComponent;
|
|
12
|
+
scaleFactor?: number;
|
|
13
|
+
onClose?: () => void;
|
|
14
|
+
parentDirection?: string;
|
|
15
|
+
parentCrossAxisFitContent?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type RendererProps = {
|
|
19
|
+
component: TComponent;
|
|
20
|
+
scaleFactor: number;
|
|
21
|
+
onClose?: () => void;
|
|
22
|
+
parentDirection?: string;
|
|
23
|
+
parentCrossAxisFitContent?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ComponentRenderer = React.ComponentType<any>;
|
|
27
|
+
|
|
28
|
+
const componentRendererCache = new Map<string, ComponentRenderer | null>();
|
|
29
|
+
|
|
30
|
+
function resolveRenderer(rendererKey: string): ComponentRenderer | null {
|
|
31
|
+
if (componentRendererCache.has(rendererKey)) {
|
|
32
|
+
return componentRendererCache.get(rendererKey) ?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let renderer: ComponentRenderer | null = null;
|
|
36
|
+
|
|
37
|
+
switch (rendererKey) {
|
|
38
|
+
case 'NamiButton':
|
|
39
|
+
renderer = require('./elements/NamiButton').NamiButton;
|
|
40
|
+
break;
|
|
41
|
+
case 'NamiText':
|
|
42
|
+
renderer = require('./elements/NamiText').NamiText;
|
|
43
|
+
break;
|
|
44
|
+
case 'NamiSymbol':
|
|
45
|
+
renderer = require('./elements/NamiSymbol').NamiSymbol;
|
|
46
|
+
break;
|
|
47
|
+
case 'NamiImage':
|
|
48
|
+
renderer = require('./elements/NamiImage').NamiImage;
|
|
49
|
+
break;
|
|
50
|
+
case 'NamiSpacer':
|
|
51
|
+
renderer = require('./elements/NamiSpacer').NamiSpacer;
|
|
52
|
+
break;
|
|
53
|
+
case 'NamiVideo':
|
|
54
|
+
renderer = require('./elements/NamiVideo').NamiVideo;
|
|
55
|
+
break;
|
|
56
|
+
case 'NamiContainer':
|
|
57
|
+
renderer = require('./containers/NamiContainer').NamiContainer;
|
|
58
|
+
break;
|
|
59
|
+
case 'NamiProductContainer':
|
|
60
|
+
renderer = require('./containers/NamiProductContainer').NamiProductContainer;
|
|
61
|
+
break;
|
|
62
|
+
case 'NamiStack':
|
|
63
|
+
renderer = require('./containers/NamiStack').NamiStack;
|
|
64
|
+
break;
|
|
65
|
+
case 'NamiCarousel':
|
|
66
|
+
renderer = require('./containers/NamiCarousel').NamiCarousel;
|
|
67
|
+
break;
|
|
68
|
+
case 'NamiCollapseContainer':
|
|
69
|
+
renderer = require('./containers/NamiCollapseContainer').NamiCollapseContainer;
|
|
70
|
+
break;
|
|
71
|
+
case 'NamiResponsiveGrid':
|
|
72
|
+
renderer = require('./containers/NamiResponsiveGrid').NamiResponsiveGrid;
|
|
73
|
+
break;
|
|
74
|
+
case 'NamiRepeatingGrid':
|
|
75
|
+
renderer = require('./containers/NamiRepeatingGrid').NamiRepeatingGrid;
|
|
76
|
+
break;
|
|
77
|
+
case 'NamiSegmentPicker':
|
|
78
|
+
renderer = require('./elements/NamiSegmentPicker').NamiSegmentPicker;
|
|
79
|
+
break;
|
|
80
|
+
case 'NamiSegmentPickerItem':
|
|
81
|
+
renderer = require('./elements/NamiSegmentPickerItem').NamiSegmentPickerItem;
|
|
82
|
+
break;
|
|
83
|
+
case 'NamiToggleSwitch':
|
|
84
|
+
renderer = require('./elements/NamiToggleSwitch').NamiToggleSwitch;
|
|
85
|
+
break;
|
|
86
|
+
case 'NamiRadioButton':
|
|
87
|
+
renderer = require('./elements/NamiRadioButton').NamiRadioButton;
|
|
88
|
+
break;
|
|
89
|
+
case 'NamiProgressIndicator':
|
|
90
|
+
renderer = require('./elements/NamiProgressIndicator').NamiProgressIndicator;
|
|
91
|
+
break;
|
|
92
|
+
case 'NamiProgressBar':
|
|
93
|
+
renderer = require('./elements/NamiProgressBar').NamiProgressBar;
|
|
94
|
+
break;
|
|
95
|
+
case 'NamiCountdownTimer':
|
|
96
|
+
renderer = require('./elements/NamiCountdownTimer').NamiCountdownTimer;
|
|
97
|
+
break;
|
|
98
|
+
case 'NamiToggleButton':
|
|
99
|
+
renderer = require('./elements/NamiToggleButton').NamiToggleButton;
|
|
100
|
+
break;
|
|
101
|
+
case 'NamiPlayPauseButton':
|
|
102
|
+
renderer = require('./elements/NamiPlayPauseButton').NamiPlayPauseButton;
|
|
103
|
+
break;
|
|
104
|
+
case 'NamiVolumeButton':
|
|
105
|
+
renderer = require('./elements/NamiVolumeButton').NamiVolumeButton;
|
|
106
|
+
break;
|
|
107
|
+
case 'NamiQRCode':
|
|
108
|
+
renderer = require('./elements/NamiQRCode').NamiQRCode;
|
|
109
|
+
break;
|
|
110
|
+
default:
|
|
111
|
+
renderer = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
componentRendererCache.set(rendererKey, renderer);
|
|
115
|
+
return renderer;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function renderWithKey(
|
|
119
|
+
rendererKey: string,
|
|
120
|
+
props: RendererProps,
|
|
121
|
+
): React.ReactElement | null {
|
|
122
|
+
const Renderer = resolveRenderer(rendererKey);
|
|
123
|
+
return Renderer ? <Renderer {...props} /> : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const TemplateRenderer: React.FC<Props> = ({ component, scaleFactor = 1, onClose, parentDirection, parentCrossAxisFitContent}) => {
|
|
127
|
+
const ctx = usePaywallContext();
|
|
128
|
+
const smartTextSku = (component as any)?.smartTextSku ?? (component as any)?.sku;
|
|
129
|
+
|
|
130
|
+
// Skip hidden components
|
|
131
|
+
if (component.hidden) return null;
|
|
132
|
+
|
|
133
|
+
// Handle condition components - evaluate condition, render children if matched
|
|
134
|
+
if (component.component === 'condition') {
|
|
135
|
+
const cond = component as any;
|
|
136
|
+
if (!conditionComponentMatches(ctx, cond, smartTextSku)) return null;
|
|
137
|
+
// Render children of condition
|
|
138
|
+
return (
|
|
139
|
+
<>
|
|
140
|
+
{cond.components?.map((child: TComponent, i: number) => (
|
|
141
|
+
<TemplateRenderer key={child.id ?? i} component={child} scaleFactor={scaleFactor} onClose={onClose} parentDirection={parentDirection} parentCrossAxisFitContent={parentCrossAxisFitContent} />
|
|
142
|
+
))}
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Apply conditionAttributes overrides if any match
|
|
148
|
+
let resolvedComponent = component;
|
|
149
|
+
if (component.conditionAttributes?.length) {
|
|
150
|
+
resolvedComponent = withOverrides(ctx, resolvedComponent as any, smartTextSku) as any;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Resolve smart-text values on component fields (shallow)
|
|
154
|
+
if (!(resolvedComponent as any).__namiSmartTextResolved) {
|
|
155
|
+
resolvedComponent = resolveComponentSmartText(resolvedComponent, ctx);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const componentType: string = resolvedComponent.component;
|
|
159
|
+
return renderResolvedComponent(
|
|
160
|
+
componentType,
|
|
161
|
+
resolvedComponent,
|
|
162
|
+
scaleFactor,
|
|
163
|
+
onClose,
|
|
164
|
+
parentDirection,
|
|
165
|
+
parentCrossAxisFitContent,
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
function renderResolvedComponent(
|
|
170
|
+
componentType: string,
|
|
171
|
+
resolvedComponent: TComponent,
|
|
172
|
+
scaleFactor: number,
|
|
173
|
+
onClose?: () => void,
|
|
174
|
+
parentDirection?: string,
|
|
175
|
+
parentCrossAxisFitContent?: boolean,
|
|
176
|
+
): React.ReactElement | null {
|
|
177
|
+
const sharedProps: RendererProps = {
|
|
178
|
+
component: resolvedComponent,
|
|
179
|
+
scaleFactor,
|
|
180
|
+
onClose,
|
|
181
|
+
parentDirection,
|
|
182
|
+
parentCrossAxisFitContent,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
switch (componentType) {
|
|
186
|
+
case 'button':
|
|
187
|
+
return renderWithKey('NamiButton', sharedProps);
|
|
188
|
+
case 'text':
|
|
189
|
+
case 'title':
|
|
190
|
+
case 'body':
|
|
191
|
+
case 'legal':
|
|
192
|
+
case 'text-list':
|
|
193
|
+
return renderWithKey('NamiText', sharedProps);
|
|
194
|
+
case 'symbol':
|
|
195
|
+
return renderWithKey('NamiSymbol', sharedProps);
|
|
196
|
+
case 'image':
|
|
197
|
+
return renderWithKey('NamiImage', sharedProps);
|
|
198
|
+
case 'spacer':
|
|
199
|
+
return renderWithKey('NamiSpacer', sharedProps);
|
|
200
|
+
case 'videoUrl':
|
|
201
|
+
return renderWithKey('NamiVideo', sharedProps);
|
|
202
|
+
case 'container':
|
|
203
|
+
case 'formContainer':
|
|
204
|
+
return renderWithKey('NamiContainer', sharedProps);
|
|
205
|
+
case 'productContainer':
|
|
206
|
+
return renderWithKey('NamiProductContainer', sharedProps);
|
|
207
|
+
case 'stack':
|
|
208
|
+
return renderWithKey('NamiStack', sharedProps);
|
|
209
|
+
case 'carouselContainer':
|
|
210
|
+
return renderWithKey('NamiCarousel', sharedProps);
|
|
211
|
+
case 'collapseContainer':
|
|
212
|
+
return renderWithKey('NamiCollapseContainer', sharedProps);
|
|
213
|
+
case 'responsiveGrid':
|
|
214
|
+
return renderWithKey('NamiResponsiveGrid', sharedProps);
|
|
215
|
+
case 'repeatingGrid':
|
|
216
|
+
return renderWithKey('NamiRepeatingGrid', sharedProps);
|
|
217
|
+
case 'segmentPicker':
|
|
218
|
+
case 'formSegmentPicker':
|
|
219
|
+
return renderWithKey('NamiSegmentPicker', sharedProps);
|
|
220
|
+
case 'segmentPickerItem':
|
|
221
|
+
return renderWithKey('NamiSegmentPickerItem', sharedProps);
|
|
222
|
+
case 'toggleSwitch':
|
|
223
|
+
return renderWithKey('NamiToggleSwitch', sharedProps);
|
|
224
|
+
case 'radio':
|
|
225
|
+
return renderWithKey('NamiRadioButton', sharedProps);
|
|
226
|
+
case 'progressIndicator':
|
|
227
|
+
return renderWithKey('NamiProgressIndicator', sharedProps);
|
|
228
|
+
case 'progressBar':
|
|
229
|
+
return renderWithKey('NamiProgressBar', sharedProps);
|
|
230
|
+
case 'countdownTimerText':
|
|
231
|
+
return renderWithKey('NamiCountdownTimer', sharedProps);
|
|
232
|
+
case 'toggleButton':
|
|
233
|
+
return renderWithKey('NamiToggleButton', sharedProps);
|
|
234
|
+
case 'playPauseButton':
|
|
235
|
+
return renderWithKey('NamiPlayPauseButton', sharedProps);
|
|
236
|
+
case 'volumeButton':
|
|
237
|
+
return renderWithKey('NamiVolumeButton', sharedProps);
|
|
238
|
+
case 'qrCode':
|
|
239
|
+
return renderWithKey('NamiQRCode', sharedProps);
|
|
240
|
+
default:
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { View, ImageBackground, StyleSheet, type ViewStyle } from 'react-native';
|
|
3
|
+
import type { TContainer, TComponent } from '@namiml/sdk-core';
|
|
4
|
+
import { applyStyles, parseColor, resolveFillImageUrl } from '../../utils/styles';
|
|
5
|
+
import { TemplateRenderer } from '../TemplateRenderer';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
component: TContainer;
|
|
9
|
+
scaleFactor: number;
|
|
10
|
+
onClose?: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const NamiBackgroundContainer: React.FC<Props> = ({ component, scaleFactor, onClose }) => {
|
|
14
|
+
if (!component || component.hidden) return null;
|
|
15
|
+
|
|
16
|
+
const containerStyle = useMemo(
|
|
17
|
+
() => applyStyles(component as any, scaleFactor) as ViewStyle,
|
|
18
|
+
[component, scaleFactor]
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const fillImageUrl = resolveFillImageUrl(component.fillImage);
|
|
22
|
+
const bgColor = parseColor(component.fillColor) ?? parseColor(component.fillColorFallback);
|
|
23
|
+
|
|
24
|
+
const direction = component.direction ?? 'vertical';
|
|
25
|
+
const content = (
|
|
26
|
+
<>
|
|
27
|
+
{component.components?.map((comp: TComponent, i: number) => {
|
|
28
|
+
const marginKey = direction === 'vertical' ? 'topMargin' : 'leftMargin';
|
|
29
|
+
const spacedComponent =
|
|
30
|
+
i === 0 || !component.spacing
|
|
31
|
+
? comp
|
|
32
|
+
: ({
|
|
33
|
+
...comp,
|
|
34
|
+
[marginKey]: mergeRawMarginValue((comp as any)[marginKey], component.spacing),
|
|
35
|
+
} as TComponent);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<TemplateRenderer
|
|
39
|
+
key={comp.id ?? i}
|
|
40
|
+
component={spacedComponent}
|
|
41
|
+
scaleFactor={scaleFactor}
|
|
42
|
+
onClose={onClose}
|
|
43
|
+
parentDirection={direction}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
})}
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (fillImageUrl) {
|
|
51
|
+
return (
|
|
52
|
+
<ImageBackground
|
|
53
|
+
source={{ uri: fillImageUrl }}
|
|
54
|
+
resizeMode='cover'
|
|
55
|
+
style={[containerStyle, styles.background]}
|
|
56
|
+
>
|
|
57
|
+
{content}
|
|
58
|
+
</ImageBackground>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<View style={[containerStyle, bgColor ? { backgroundColor: bgColor } : null, styles.background]}>
|
|
64
|
+
{content}
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function mergeRawMarginValue(existing: unknown, spacing: unknown): unknown {
|
|
70
|
+
if (existing == null) return spacing;
|
|
71
|
+
|
|
72
|
+
const existingNumeric = toNumericMargin(existing);
|
|
73
|
+
const spacingNumeric = toNumericMargin(spacing);
|
|
74
|
+
if (existingNumeric == null || spacingNumeric == null) {
|
|
75
|
+
return existing;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return existingNumeric + spacingNumeric;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toNumericMargin(value: unknown): number | null {
|
|
82
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof value !== 'string') {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const trimmed = value.trim();
|
|
91
|
+
if (!trimmed || !/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const parsed = Number(trimmed);
|
|
96
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const styles = StyleSheet.create({
|
|
100
|
+
background: {
|
|
101
|
+
...StyleSheet.absoluteFillObject,
|
|
102
|
+
},
|
|
103
|
+
});
|