@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.
Files changed (60) hide show
  1. package/dist/index.cjs +4000 -0
  2. package/dist/index.cjs.map +1 -0
  3. package/dist/index.d.ts +151 -0
  4. package/dist/index.mjs +3966 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/nami-expo-nami-iap.tgz +0 -0
  7. package/package.json +92 -0
  8. package/src/adapters/expo-device.adapter.ts +106 -0
  9. package/src/adapters/expo-purchase.adapter.ts +79 -0
  10. package/src/adapters/expo-storage.adapter.ts +92 -0
  11. package/src/adapters/expo-ui.adapter.ts +57 -0
  12. package/src/adapters/index.ts +33 -0
  13. package/src/amazon-kepler.d.ts +7 -0
  14. package/src/components/NamiView.tsx +1006 -0
  15. package/src/components/PaywallScreen.tsx +245 -0
  16. package/src/components/TemplateRenderer.tsx +243 -0
  17. package/src/components/containers/NamiBackgroundContainer.tsx +103 -0
  18. package/src/components/containers/NamiCarousel.tsx +217 -0
  19. package/src/components/containers/NamiCollapseContainer.tsx +116 -0
  20. package/src/components/containers/NamiContainer.tsx +315 -0
  21. package/src/components/containers/NamiContentContainer.tsx +140 -0
  22. package/src/components/containers/NamiFooter.tsx +35 -0
  23. package/src/components/containers/NamiHeader.tsx +45 -0
  24. package/src/components/containers/NamiProductContainer.tsx +248 -0
  25. package/src/components/containers/NamiRepeatingGrid.tsx +81 -0
  26. package/src/components/containers/NamiResponsiveGrid.tsx +75 -0
  27. package/src/components/containers/NamiStack.tsx +69 -0
  28. package/src/components/elements/NamiButton.tsx +285 -0
  29. package/src/components/elements/NamiCountdownTimer.tsx +123 -0
  30. package/src/components/elements/NamiImage.tsx +177 -0
  31. package/src/components/elements/NamiPlayPauseButton.tsx +93 -0
  32. package/src/components/elements/NamiProgressBar.tsx +90 -0
  33. package/src/components/elements/NamiProgressIndicator.tsx +41 -0
  34. package/src/components/elements/NamiQRCode.tsx +51 -0
  35. package/src/components/elements/NamiRadioButton.tsx +62 -0
  36. package/src/components/elements/NamiSegmentPicker.tsx +67 -0
  37. package/src/components/elements/NamiSegmentPickerItem.tsx +184 -0
  38. package/src/components/elements/NamiSpacer.tsx +23 -0
  39. package/src/components/elements/NamiSymbol.tsx +104 -0
  40. package/src/components/elements/NamiText.tsx +311 -0
  41. package/src/components/elements/NamiToggleButton.tsx +102 -0
  42. package/src/components/elements/NamiToggleSwitch.tsx +64 -0
  43. package/src/components/elements/NamiVideo.kepler.tsx +638 -0
  44. package/src/components/elements/NamiVideo.tsx +133 -0
  45. package/src/components/elements/NamiVolumeButton.tsx +93 -0
  46. package/src/context/FocusContext.tsx +169 -0
  47. package/src/context/PaywallContext.tsx +343 -0
  48. package/src/global.d.ts +5 -0
  49. package/src/index.ts +62 -0
  50. package/src/nami.ts +24 -0
  51. package/src/react-native-qrcode-svg.d.ts +4 -0
  52. package/src/utils/actionHandler.ts +281 -0
  53. package/src/utils/fonts.ts +359 -0
  54. package/src/utils/iconMap.ts +67 -0
  55. package/src/utils/impression.ts +39 -0
  56. package/src/utils/rendering.ts +197 -0
  57. package/src/utils/smartText.ts +148 -0
  58. package/src/utils/styles.ts +668 -0
  59. package/src/utils/tvFocus.ts +31 -0
  60. package/src/utils/videoControls.ts +49 -0
@@ -0,0 +1,311 @@
1
+ import React, { useCallback, useMemo } from 'react';
2
+ import { Text, View, StyleSheet, Linking } from 'react-native';
3
+ import { usePaywallContext } from '../../context/PaywallContext';
4
+ import { applyStyles, textStyles, childSpacingStyle, parseSize, parseColor } from '../../utils/styles';
5
+ import { resolveFontDescriptor } from '../../utils/fonts';
6
+ import { buildSmartTextReplacements, interpolateSmartText } from '../../utils/smartText';
7
+ import { formatDate, isValidISODate } from '@namiml/sdk-core';
8
+ import { useInheritedFocusedStyle } from '../../context/FocusContext';
9
+
10
+ interface Props {
11
+ component: any;
12
+ scaleFactor: number;
13
+ isSymbol?: boolean;
14
+ parentDirection?: string;
15
+ }
16
+
17
+ const SYMBOL_FALLBACK_MAP: Record<string, string> = {
18
+ check: '\u2713',
19
+ close: '\u2715',
20
+ plus: '+',
21
+ minus: '\u2212',
22
+ right: '\u203A',
23
+ left: '\u2039',
24
+ up: '\u2303',
25
+ down: '\u2304',
26
+ dot: '\u2022',
27
+ };
28
+
29
+ type MarkdownToken =
30
+ | { type: 'text'; value: string }
31
+ | { type: 'bold'; value: string }
32
+ | { type: 'italic'; value: string }
33
+ | { type: 'link'; value: string; href: string };
34
+
35
+ function parseEmphasis(input: string): MarkdownToken[] {
36
+ const tokens: MarkdownToken[] = [];
37
+ const EMPHASIS_REGEX = /(\*\*([^*]+)\*\*|\*([^*]+)\*)/g;
38
+ let start = 0;
39
+ let match: RegExpExecArray | null;
40
+
41
+ while ((match = EMPHASIS_REGEX.exec(input)) !== null) {
42
+ if (match.index > start) {
43
+ tokens.push({ type: 'text', value: input.slice(start, match.index) });
44
+ }
45
+ if (match[2] != null) {
46
+ tokens.push({ type: 'bold', value: match[2] });
47
+ } else if (match[3] != null) {
48
+ tokens.push({ type: 'italic', value: match[3] });
49
+ }
50
+ start = match.index + match[0].length;
51
+ }
52
+
53
+ if (start < input.length) {
54
+ tokens.push({ type: 'text', value: input.slice(start) });
55
+ }
56
+
57
+ return tokens;
58
+ }
59
+
60
+ function parseInlineMarkdown(input: string): MarkdownToken[] {
61
+ const tokens: MarkdownToken[] = [];
62
+ const LINK_REGEX = /\[([^\]]+)\]\(([^)\s]+)\)/g;
63
+ let start = 0;
64
+ let match: RegExpExecArray | null;
65
+
66
+ while ((match = LINK_REGEX.exec(input)) !== null) {
67
+ if (match.index > start) {
68
+ tokens.push(...parseEmphasis(input.slice(start, match.index)));
69
+ }
70
+ tokens.push({ type: 'link', value: match[1], href: match[2] });
71
+ start = match.index + match[0].length;
72
+ }
73
+
74
+ if (start < input.length) {
75
+ tokens.push(...parseEmphasis(input.slice(start)));
76
+ }
77
+
78
+ return tokens;
79
+ }
80
+
81
+ export const NamiText: React.FC<Props> = ({ component, scaleFactor, isSymbol, parentDirection }) => {
82
+ const ctx = usePaywallContext();
83
+ const isFocused = useInheritedFocusedStyle();
84
+ const smartTextSku = component?.smartTextSku ?? component?.sku;
85
+ const replacements = useMemo(
86
+ () => buildSmartTextReplacements(ctx.state, ctx.flow, smartTextSku),
87
+ [ctx.state, ctx.flow, smartTextSku]
88
+ );
89
+
90
+ const resolvedText = useMemo(() => {
91
+ const raw = component.text ?? component.title ?? '';
92
+ const resolved = interpolateSmartText(raw, replacements);
93
+ const str = resolved == null ? '' : String(resolved);
94
+ if (component.dateTimeFormat && isValidISODate(str)) {
95
+ return formatDate(str, component.dateTimeFormat);
96
+ }
97
+ return str;
98
+ }, [component.text, component.title, replacements, component.dateTimeFormat]);
99
+
100
+ const resolvedListItems = useMemo(() => {
101
+ if (Array.isArray(component.texts)) {
102
+ return component.texts
103
+ .map((value: any) => {
104
+ const resolved = interpolateSmartText(value, replacements);
105
+ return resolved == null ? '' : String(resolved);
106
+ })
107
+ .filter((value: string) => value.trim().length > 0);
108
+ }
109
+ return resolvedText
110
+ .split('\n')
111
+ .map((value: string) => value.trim())
112
+ .filter(Boolean);
113
+ }, [component.texts, resolvedText, replacements]);
114
+ const inlineTokens = useMemo(
115
+ () => parseInlineMarkdown(resolvedText),
116
+ [resolvedText],
117
+ );
118
+ const hasInlineFormatting = useMemo(
119
+ () => hasRichInlineContent(inlineTokens, resolvedText),
120
+ [inlineTokens, resolvedText],
121
+ );
122
+ const resolvedListItemTokens = useMemo(
123
+ () => resolvedListItems.map((item: string) => parseInlineMarkdown(item)),
124
+ [resolvedListItems],
125
+ );
126
+
127
+ if (!resolvedText && !isSymbol && component.component !== 'text-list') return null;
128
+
129
+ const containerStyle = useMemo(
130
+ () => applyStyles(component, scaleFactor, isFocused, parentDirection),
131
+ [component, scaleFactor, isFocused, parentDirection],
132
+ );
133
+ const txtStyle = useMemo(
134
+ () => textStyles(component, scaleFactor, isFocused),
135
+ [component, scaleFactor, isFocused]
136
+ );
137
+ const linkColor = parseColor(component.linkColor) ?? '#0000EE';
138
+ const linkStyle = useMemo(() => [styles.link, { color: linkColor }], [linkColor]);
139
+ const emphasisBaseStyle = useMemo(
140
+ () => ({
141
+ ...(txtStyle.color ? { color: txtStyle.color } : {}),
142
+ ...(txtStyle.fontSize ? { fontSize: txtStyle.fontSize } : {}),
143
+ ...(txtStyle.lineHeight ? { lineHeight: txtStyle.lineHeight } : {}),
144
+ }),
145
+ [txtStyle.color, txtStyle.fontSize, txtStyle.lineHeight],
146
+ );
147
+ const baseFontName = component.fontName ?? component.fontFamily;
148
+ const boldHostedFont = useMemo(
149
+ () => resolveFontDescriptor(baseFontName, { bold: true, italic: false }),
150
+ [baseFontName],
151
+ );
152
+ const italicHostedFont = useMemo(
153
+ () => resolveFontDescriptor(baseFontName, { bold: false, italic: true }),
154
+ [baseFontName],
155
+ );
156
+ const boldStyle = useMemo(
157
+ () => [
158
+ emphasisBaseStyle,
159
+ boldHostedFont.family
160
+ ? {
161
+ fontFamily: boldHostedFont.family,
162
+ fontWeight: 'normal' as const,
163
+ fontStyle: 'normal' as const,
164
+ }
165
+ : styles.bold,
166
+ ],
167
+ [boldHostedFont.family, emphasisBaseStyle],
168
+ );
169
+ const italicStyle = useMemo(
170
+ () => [
171
+ emphasisBaseStyle,
172
+ italicHostedFont.family
173
+ ? {
174
+ fontFamily: italicHostedFont.family,
175
+ fontWeight: 'normal' as const,
176
+ fontStyle: 'normal' as const,
177
+ ...(italicHostedFont.variant !== 'italic' && italicHostedFont.variant !== 'boldItalic'
178
+ ? { transform: [{ skewX: '-10deg' as const }] }
179
+ : {}),
180
+ }
181
+ : styles.italic,
182
+ ],
183
+ [italicHostedFont.family, italicHostedFont.variant, emphasisBaseStyle],
184
+ );
185
+
186
+ const openHref = useCallback((href: string) => {
187
+ Linking.canOpenURL(href).then((canOpen) => {
188
+ if (canOpen) {
189
+ Linking.openURL(href);
190
+ }
191
+ });
192
+ }, []);
193
+
194
+ if (component.component === 'text-list') {
195
+ const bullet = component.bulletComponent;
196
+ const bulletText = bullet?.text
197
+ ?? SYMBOL_FALLBACK_MAP[(bullet?.name ?? '').toLowerCase()]
198
+ ?? '\u2022';
199
+ const inlineGap = parseSize(component.spacing, scaleFactor) ?? 0;
200
+
201
+ return (
202
+ <View style={containerStyle}>
203
+ {resolvedListItems.map((item: string, i: number) => (
204
+ <View key={i} style={childSpacingStyle(i, { spacing: component.spacing, direction: 'vertical' }, scaleFactor)}>
205
+ <View style={styles.listRow}>
206
+ <Text style={[txtStyle, { marginRight: inlineGap }]} allowFontScaling={false}>{bulletText}</Text>
207
+ {hasRichInlineContent(resolvedListItemTokens[i], item) ? (
208
+ <Text style={[txtStyle, styles.listText]} allowFontScaling={false}>
209
+ {resolvedListItemTokens[i].map((token: MarkdownToken, index: number) => {
210
+ if (token.type === 'link') {
211
+ return (
212
+ <Text
213
+ key={`link-${i}-${index}`}
214
+ style={linkStyle}
215
+ onPress={() => openHref(token.href)}
216
+ suppressHighlighting
217
+ >
218
+ {token.value}
219
+ </Text>
220
+ );
221
+ }
222
+ if (token.type === 'bold') {
223
+ return <Text key={`bold-${i}-${index}`} style={boldStyle}>{token.value}</Text>;
224
+ }
225
+ if (token.type === 'italic') {
226
+ return <Text key={`italic-${i}-${index}`} style={italicStyle}>{token.value}</Text>;
227
+ }
228
+ return <Text key={`text-${i}-${index}`}>{token.value}</Text>;
229
+ })}
230
+ </Text>
231
+ ) : (
232
+ <Text style={[txtStyle, styles.listText]} allowFontScaling={false}>
233
+ {item}
234
+ </Text>
235
+ )}
236
+ </View>
237
+ </View>
238
+ ))}
239
+ </View>
240
+ );
241
+ }
242
+
243
+ const fitContent = component.width === 'fitContent';
244
+ const numberOfLines = component.maxLines ?? (fitContent ? 1 : undefined);
245
+ const ellipsizeMode = fitContent ? 'clip' : undefined;
246
+
247
+ return (
248
+ <View style={containerStyle}>
249
+ <Text
250
+ style={txtStyle}
251
+ numberOfLines={numberOfLines}
252
+ ellipsizeMode={ellipsizeMode}
253
+ allowFontScaling={false}
254
+ >
255
+ {hasInlineFormatting ? (
256
+ inlineTokens.map((token: MarkdownToken, index: number) => {
257
+ if (token.type === 'link') {
258
+ return (
259
+ <Text
260
+ key={`link-${index}`}
261
+ style={linkStyle}
262
+ onPress={() => openHref(token.href)}
263
+ suppressHighlighting
264
+ >
265
+ {token.value}
266
+ </Text>
267
+ );
268
+ }
269
+ if (token.type === 'bold') {
270
+ return <Text key={`bold-${index}`} style={boldStyle}>{token.value}</Text>;
271
+ }
272
+ if (token.type === 'italic') {
273
+ return <Text key={`italic-${index}`} style={italicStyle}>{token.value}</Text>;
274
+ }
275
+ return <Text key={`text-${index}`}>{token.value}</Text>;
276
+ })
277
+ ) : (
278
+ resolvedText
279
+ )}
280
+ </Text>
281
+ </View>
282
+ );
283
+ };
284
+
285
+ function hasRichInlineContent(tokens: MarkdownToken[], originalText: string): boolean {
286
+ return !(
287
+ tokens.length === 1
288
+ && tokens[0]?.type === 'text'
289
+ && tokens[0]?.value === originalText
290
+ );
291
+ }
292
+
293
+ const styles = StyleSheet.create({
294
+ listRow: {
295
+ flexDirection: 'row',
296
+ alignItems: 'flex-start',
297
+ },
298
+ listText: {
299
+ flexShrink: 1,
300
+ flexWrap: 'wrap',
301
+ },
302
+ link: {
303
+ textDecorationLine: 'underline',
304
+ },
305
+ bold: {
306
+ fontWeight: '700',
307
+ },
308
+ italic: {
309
+ fontStyle: 'italic',
310
+ },
311
+ });
@@ -0,0 +1,102 @@
1
+ import React, { useCallback, useMemo, useRef } from 'react';
2
+ import { TouchableOpacity, View } from 'react-native';
3
+ import { usePaywallContext } from '../../context/PaywallContext';
4
+ import { applyStyles, parseColor, parseSize, childSpacingStyle, shadowStyles, focusedStyleOverrides } from '../../utils/styles';
5
+ import { handleAction } from '../../utils/actionHandler';
6
+ import { TemplateRenderer } from '../TemplateRenderer';
7
+ import type { TComponent } from '@namiml/sdk-core';
8
+ import { FocusedStyleProvider, useFocusableState, useFocusEnabled, useRegisterPreferredFocus } from '../../context/FocusContext';
9
+ import { useTVPreferredFocus } from '../../utils/tvFocus';
10
+
11
+ interface Props {
12
+ component: any;
13
+ scaleFactor: number;
14
+ onClose?: () => void;
15
+ parentDirection?: string;
16
+ }
17
+
18
+ export const NamiToggleButton: React.FC<Props> = ({ component, scaleFactor, onClose, parentDirection }) => {
19
+ const ctx = usePaywallContext();
20
+ const focusEnabled = useFocusEnabled();
21
+ const touchableRef = useRef<any>(null);
22
+ const { isFocused, onFocus, onBlur } = useFocusableState(
23
+ Boolean(component.focused),
24
+ focusEnabled,
25
+ );
26
+ useTVPreferredFocus(
27
+ touchableRef,
28
+ focusEnabled && Boolean(component.focused),
29
+ );
30
+ useRegisterPreferredFocus(
31
+ touchableRef.current,
32
+ focusEnabled && Boolean(component.focused),
33
+ );
34
+ const formId = component.formId ?? component.id ?? '';
35
+ const mode = component.mode ?? 'toggle';
36
+ const currentValue = ctx.state.formStates?.[formId];
37
+ const isActive = mode === 'radio'
38
+ ? currentValue === component.value
39
+ : !!currentValue;
40
+
41
+ const onPress = useCallback(() => {
42
+ const functionName = component.onTap?.function;
43
+ if (!functionName) return;
44
+
45
+ handleAction({
46
+ onTap: {
47
+ function: functionName,
48
+ parameters: {
49
+ ...(component.onTap?.parameters ?? {}),
50
+ formId,
51
+ value: mode === 'radio' ? component.value : undefined,
52
+ },
53
+ },
54
+ ctx,
55
+ onClose,
56
+ });
57
+ }, [formId, mode, component.value, ctx, component.onTap, onClose]);
58
+
59
+ const containerStyle = useMemo(() => {
60
+ const base = applyStyles(component, scaleFactor, false, parentDirection);
61
+ const fillColor = isActive
62
+ ? (component.activeFillColor ?? component.activeFillColorFallback ?? component.selectedFillColor ?? component.focusedFillColor)
63
+ : (component.inactiveFillColor ?? component.inactiveFillColorFallback ?? component.fillColor);
64
+ const borderColor = isActive ? component.activeBorderColor : component.inactiveBorderColor;
65
+ const borderWidth = isActive ? component.activeBorderWidth : component.inactiveBorderWidth;
66
+ const dropShadow = isActive ? component.activeDropShadow : component.inactiveDropShadow;
67
+
68
+ const bg = parseColor(fillColor);
69
+ if (bg) base.backgroundColor = bg;
70
+ const bc = parseColor(borderColor);
71
+ if (bc) base.borderColor = bc;
72
+ const bw = parseSize(borderWidth, scaleFactor);
73
+ if (bw != null) base.borderWidth = bw;
74
+ if (dropShadow) {
75
+ Object.assign(base, shadowStyles({ ...component, dropShadow } as any));
76
+ }
77
+ if (isFocused) {
78
+ Object.assign(base, focusedStyleOverrides(component, scaleFactor));
79
+ }
80
+ return base;
81
+ }, [component, scaleFactor, isActive, isFocused, parentDirection]);
82
+
83
+ return (
84
+ <TouchableOpacity
85
+ ref={touchableRef}
86
+ onPress={onPress}
87
+ onFocus={onFocus}
88
+ onBlur={onBlur}
89
+ hasTVPreferredFocus={focusEnabled && Boolean(component.focused)}
90
+ activeOpacity={0.7}
91
+ style={containerStyle}
92
+ >
93
+ <FocusedStyleProvider focused={isFocused || isActive}>
94
+ {component.components?.map((child: TComponent, i: number) => (
95
+ <View key={child.id ?? i} style={childSpacingStyle(i, { spacing: component.spacing, direction: component.direction }, scaleFactor)}>
96
+ <TemplateRenderer component={child} scaleFactor={scaleFactor} onClose={onClose} parentDirection={parentDirection} />
97
+ </View>
98
+ ))}
99
+ </FocusedStyleProvider>
100
+ </TouchableOpacity>
101
+ );
102
+ };
@@ -0,0 +1,64 @@
1
+ import React, { useCallback, useEffect } from 'react';
2
+ import { Switch, View } from 'react-native';
3
+ import { usePaywallContext } from '../../context/PaywallContext';
4
+ import { applyStyles, parseColor } from '../../utils/styles';
5
+ import { handleAction } from '../../utils/actionHandler';
6
+
7
+ interface Props {
8
+ component: any;
9
+ scaleFactor: number;
10
+ onClose?: () => void;
11
+ parentDirection?: string;
12
+ }
13
+
14
+ export const NamiToggleSwitch: React.FC<Props> = ({ component, scaleFactor, onClose, parentDirection }) => {
15
+ const ctx = usePaywallContext();
16
+ const formId = component.formId ?? component.id ?? '';
17
+ const isOn = !!ctx.state.formStates?.[formId];
18
+
19
+ useEffect(() => {
20
+ if (!formId) return;
21
+ if (ctx.state.formStates?.[formId] === undefined) {
22
+ ctx.setFormState(formId, !!component.checked);
23
+ }
24
+ }, [formId, ctx, component.checked]);
25
+
26
+ const onValueChange = useCallback((val: boolean) => {
27
+ ctx.setFormState(formId, val);
28
+ if (component.onTap) {
29
+ handleAction({
30
+ onTap: {
31
+ function: component.onTap.function,
32
+ parameters: {
33
+ ...(component.onTap.parameters ?? {}),
34
+ formId,
35
+ value: val ? 'true' : '',
36
+ },
37
+ },
38
+ ctx,
39
+ onClose,
40
+ });
41
+ }
42
+ }, [formId, ctx, component.onTap, onClose]);
43
+
44
+ const containerStyle = applyStyles(component, scaleFactor, false, parentDirection);
45
+ const trackColor = {
46
+ false: parseColor(component.inactivePlateFillColor ?? component.offTrackColor) ?? '#ccc',
47
+ true: parseColor(component.activePlateFillColor ?? component.onTrackColor ?? component.fillColor) ?? '#4CD964',
48
+ };
49
+ const thumbColor = parseColor(isOn
50
+ ? (component.activeHandleFillColor ?? component.thumbColor)
51
+ : (component.inactiveHandleFillColor ?? component.thumbColor)
52
+ ) ?? '#fff';
53
+
54
+ return (
55
+ <View style={containerStyle}>
56
+ <Switch
57
+ value={isOn}
58
+ onValueChange={onValueChange}
59
+ trackColor={trackColor}
60
+ thumbColor={thumbColor}
61
+ />
62
+ </View>
63
+ );
64
+ };