@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,90 @@
|
|
|
1
|
+
import React, { useMemo, useState, useEffect } from 'react';
|
|
2
|
+
import { View, StyleSheet, Animated } from 'react-native';
|
|
3
|
+
import { applyStyles, parseColor, parseSize } from '../../utils/styles';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_INDETERMINATE_DURATION = 2;
|
|
6
|
+
const DEFAULT_INDETERMINATE_WIDTH_PCT = 30;
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
component: any;
|
|
10
|
+
scaleFactor: number;
|
|
11
|
+
parentDirection?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const NamiProgressBar: React.FC<Props> = ({ component, scaleFactor, parentDirection }) => {
|
|
15
|
+
const containerStyle = applyStyles(component, scaleFactor, false, parentDirection);
|
|
16
|
+
const mode = component.mode ?? 'determinate';
|
|
17
|
+
const innerPadding = parseSize(component.innerPadding ?? 0, scaleFactor) ?? 0;
|
|
18
|
+
const trackColor = parseColor(component.inactiveFillColor ?? component.inactiveFillColorFallback ?? component.trackColor) ?? '#E0E0E0';
|
|
19
|
+
const fillColor = parseColor(component.activeFillColor ?? component.activeFillColorFallback ?? component.progressColor ?? component.fillColor) ?? '#007AFF';
|
|
20
|
+
const barHeight = parseSize(component.height ?? component.barHeight ?? 4, scaleFactor) ?? 4;
|
|
21
|
+
const inactiveRadius = parseSize(component.inactiveBorderRadius ?? component.borderRadius, scaleFactor) ?? barHeight / 2;
|
|
22
|
+
const activeRadius = parseSize(component.activeBorderRadius ?? component.borderRadius, scaleFactor) ?? barHeight / 2;
|
|
23
|
+
|
|
24
|
+
const [trackWidth, setTrackWidth] = useState(0);
|
|
25
|
+
const translateX = useState(new Animated.Value(0))[0];
|
|
26
|
+
|
|
27
|
+
const progressPct = useMemo(() => {
|
|
28
|
+
const raw = component.percentage ?? component.progress ?? 0;
|
|
29
|
+
if (typeof raw === 'number') {
|
|
30
|
+
if (!Number.isInteger(raw) && raw <= 1) return Math.max(0, Math.min(100, raw * 100));
|
|
31
|
+
return Math.max(0, Math.min(100, raw));
|
|
32
|
+
}
|
|
33
|
+
return 0;
|
|
34
|
+
}, [component.percentage, component.progress]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (mode !== 'indeterminate' || trackWidth <= 0) return;
|
|
38
|
+
const activeWidth = component.activeWidth ?? `${DEFAULT_INDETERMINATE_WIDTH_PCT}%`;
|
|
39
|
+
const isPct = typeof activeWidth === 'string' && activeWidth.trim().endsWith('%');
|
|
40
|
+
const barWidth = isPct
|
|
41
|
+
? trackWidth * (parseFloat(activeWidth) / 100)
|
|
42
|
+
: (typeof activeWidth === 'number' ? activeWidth : parseFloat(activeWidth));
|
|
43
|
+
const duration = (component.duration ?? DEFAULT_INDETERMINATE_DURATION) * 1000;
|
|
44
|
+
translateX.setValue(-barWidth);
|
|
45
|
+
const anim = Animated.loop(
|
|
46
|
+
Animated.timing(translateX, {
|
|
47
|
+
toValue: trackWidth,
|
|
48
|
+
duration,
|
|
49
|
+
useNativeDriver: true,
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
anim.start();
|
|
53
|
+
return () => anim.stop();
|
|
54
|
+
}, [mode, trackWidth, component.activeWidth, component.duration, translateX]);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<View style={containerStyle}>
|
|
58
|
+
<View
|
|
59
|
+
style={[styles.track, { backgroundColor: trackColor, height: barHeight, borderRadius: inactiveRadius, padding: innerPadding }]}
|
|
60
|
+
onLayout={(e) => setTrackWidth(e.nativeEvent.layout.width)}
|
|
61
|
+
>
|
|
62
|
+
{mode === 'indeterminate' ? (
|
|
63
|
+
<Animated.View
|
|
64
|
+
style={[
|
|
65
|
+
styles.fill,
|
|
66
|
+
{
|
|
67
|
+
backgroundColor: fillColor,
|
|
68
|
+
borderRadius: activeRadius,
|
|
69
|
+
width: component.activeWidth ?? `${DEFAULT_INDETERMINATE_WIDTH_PCT}%`,
|
|
70
|
+
transform: [{ translateX }],
|
|
71
|
+
},
|
|
72
|
+
]}
|
|
73
|
+
/>
|
|
74
|
+
) : (
|
|
75
|
+
<View
|
|
76
|
+
style={[
|
|
77
|
+
styles.fill,
|
|
78
|
+
{ backgroundColor: fillColor, width: `${progressPct}%`, borderRadius: activeRadius },
|
|
79
|
+
]}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
</View>
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const styles = StyleSheet.create({
|
|
88
|
+
track: { width: '100%', overflow: 'hidden' },
|
|
89
|
+
fill: { height: '100%' },
|
|
90
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { applyStyles, parseColor, parseSize } from '../../utils/styles';
|
|
4
|
+
import { NamiFlowManager } from '@namiml/sdk-core';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
component: any;
|
|
8
|
+
scaleFactor: number;
|
|
9
|
+
parentDirection?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const NamiProgressIndicator: React.FC<Props> = ({ component, scaleFactor, parentDirection }) => {
|
|
13
|
+
const containerStyle = applyStyles(component, scaleFactor, false, parentDirection);
|
|
14
|
+
const innerPadding = parseSize(component.innerPadding ?? 0, scaleFactor) ?? 0;
|
|
15
|
+
const trackColor = parseColor(component.inactiveFillColor ?? component.inactiveFillColorFallback) ?? '#E0E0E0';
|
|
16
|
+
const fillColor = parseColor(component.activeFillColor ?? component.activeFillColorFallback ?? component.fillColor) ?? '#007AFF';
|
|
17
|
+
const barHeight = parseSize(component.height ?? component.indicatorSize ?? 4, scaleFactor) ?? 4;
|
|
18
|
+
const inactiveRadius = parseSize(component.inactiveBorderRadius ?? component.borderRadius, scaleFactor) ?? barHeight / 2;
|
|
19
|
+
const activeRadius = parseSize(component.activeBorderRadius ?? component.borderRadius, scaleFactor) ?? barHeight / 2;
|
|
20
|
+
|
|
21
|
+
const [percent, setPercent] = useState<number>(0);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
let target = component.percentage ?? 0;
|
|
25
|
+
if (target === 'auto') {
|
|
26
|
+
target = NamiFlowManager.getCurrentFlowProgress(component.id) * 100;
|
|
27
|
+
}
|
|
28
|
+
const value = typeof target === 'number'
|
|
29
|
+
? (target <= 1 && !Number.isInteger(target) ? target * 100 : target)
|
|
30
|
+
: 0;
|
|
31
|
+
setPercent(Math.min(100, Math.max(0, value)));
|
|
32
|
+
}, [component, component.percentage]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View style={containerStyle}>
|
|
36
|
+
<View style={{ backgroundColor: trackColor, height: barHeight, borderRadius: inactiveRadius, padding: innerPadding }}>
|
|
37
|
+
<View style={{ backgroundColor: fillColor, height: '100%', width: `${percent}%`, borderRadius: activeRadius }} />
|
|
38
|
+
</View>
|
|
39
|
+
</View>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import { LIQUID_VARIABLE_REGEX } from '@namiml/sdk-core';
|
|
4
|
+
import QRCode from 'react-native-qrcode-svg';
|
|
5
|
+
|
|
6
|
+
import { applyStyles, parseColor, parseSize } from '../../utils/styles';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
component: any;
|
|
10
|
+
scaleFactor: number;
|
|
11
|
+
parentDirection?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const NamiQRCode: React.FC<Props> = ({ component, scaleFactor, parentDirection }) => {
|
|
15
|
+
const containerStyle = useMemo(
|
|
16
|
+
() => applyStyles(component, scaleFactor, false, parentDirection),
|
|
17
|
+
[component, scaleFactor, parentDirection]
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const borderWidth = parseSize(component.borderWidth ?? 0, scaleFactor) ?? 0;
|
|
21
|
+
const width = parseSize(component.width, scaleFactor) ?? 360;
|
|
22
|
+
const size = Math.max(1, Math.floor(width - borderWidth * 2));
|
|
23
|
+
const outerSize = Math.max(1, Math.floor(width));
|
|
24
|
+
const value = component.url ?? '';
|
|
25
|
+
const hasResolvedValue = typeof value === 'string'
|
|
26
|
+
? value.trim().length > 0 && !value.match(LIQUID_VARIABLE_REGEX)
|
|
27
|
+
: Boolean(value);
|
|
28
|
+
const color = parseColor(component.color) ?? '#000000';
|
|
29
|
+
const backgroundColor = parseColor(component.fillColor ?? component.borderColor) ?? 'transparent';
|
|
30
|
+
|
|
31
|
+
if (!hasResolvedValue) {
|
|
32
|
+
return (
|
|
33
|
+
<View style={[containerStyle, styles.fallback, { width: outerSize, height: outerSize, backgroundColor }]} />
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<View style={[containerStyle, { backgroundColor, width: outerSize, height: outerSize }]}>
|
|
39
|
+
<QRCode value={String(value)} size={size} color={color} backgroundColor={backgroundColor} />
|
|
40
|
+
</View>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const styles = StyleSheet.create({
|
|
45
|
+
fallback: {
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
justifyContent: 'center',
|
|
48
|
+
borderWidth: 1,
|
|
49
|
+
borderColor: '#000',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import { usePaywallContext } from '../../context/PaywallContext';
|
|
4
|
+
import { applyStyles, parseColor, parseSize, shadowStyles } from '../../utils/styles';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
component: any;
|
|
8
|
+
scaleFactor: number;
|
|
9
|
+
parentDirection?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const NamiRadioButton: React.FC<Props> = ({ component, scaleFactor, parentDirection }) => {
|
|
13
|
+
const ctx = usePaywallContext();
|
|
14
|
+
const skuId = component.sku?.id ?? '';
|
|
15
|
+
const selected = Object.values(ctx.state.selectedProducts ?? {}).includes(skuId);
|
|
16
|
+
|
|
17
|
+
const size = parseSize(component.radioSize ?? component.width ?? 24, scaleFactor) ?? 24;
|
|
18
|
+
const activeFill = parseColor(component.activeFillColor ?? component.fillColor) ?? '#007AFF';
|
|
19
|
+
const inactiveFill = parseColor(component.inactiveFillColor) ?? 'transparent';
|
|
20
|
+
const activeBorder = parseColor(component.activeBorderColor) ?? activeFill;
|
|
21
|
+
const inactiveBorder = parseColor(component.inactiveBorderColor ?? component.borderColor) ?? '#ccc';
|
|
22
|
+
const activeBorderWidth = parseSize(component.activeBorderWidth ?? component.borderWidth ?? 2, scaleFactor) ?? 2;
|
|
23
|
+
const inactiveBorderWidth = parseSize(component.inactiveBorderWidth ?? component.borderWidth ?? 2, scaleFactor) ?? 2;
|
|
24
|
+
const innerPadding = parseSize(component.innerPadding ?? 0, scaleFactor) ?? 0;
|
|
25
|
+
|
|
26
|
+
const containerStyle = applyStyles(component, scaleFactor, false, parentDirection);
|
|
27
|
+
const shadow = selected && component.dropShadow ? shadowStyles({ ...component, dropShadow: component.dropShadow } as any) : {};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<View style={containerStyle}>
|
|
31
|
+
<View
|
|
32
|
+
style={[
|
|
33
|
+
styles.outer,
|
|
34
|
+
{
|
|
35
|
+
width: size,
|
|
36
|
+
height: size,
|
|
37
|
+
borderRadius: size / 2,
|
|
38
|
+
borderColor: selected ? activeBorder : inactiveBorder,
|
|
39
|
+
borderWidth: selected ? activeBorderWidth : inactiveBorderWidth,
|
|
40
|
+
padding: innerPadding,
|
|
41
|
+
},
|
|
42
|
+
shadow,
|
|
43
|
+
]}
|
|
44
|
+
>
|
|
45
|
+
<View
|
|
46
|
+
style={[
|
|
47
|
+
styles.inner,
|
|
48
|
+
{
|
|
49
|
+
backgroundColor: selected ? activeFill : inactiveFill,
|
|
50
|
+
borderRadius: (size - innerPadding * 2) / 2,
|
|
51
|
+
},
|
|
52
|
+
]}
|
|
53
|
+
/>
|
|
54
|
+
</View>
|
|
55
|
+
</View>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const styles = StyleSheet.create({
|
|
60
|
+
outer: { borderWidth: 2, justifyContent: 'center', alignItems: 'center' },
|
|
61
|
+
inner: {},
|
|
62
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { ScrollView, StyleSheet, View } from 'react-native';
|
|
3
|
+
import { applyStyles, parseSize } from '../../utils/styles';
|
|
4
|
+
import { NamiSegmentPickerItem } from './NamiSegmentPickerItem';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
component: any;
|
|
8
|
+
scaleFactor: number;
|
|
9
|
+
onClose?: () => void;
|
|
10
|
+
parentDirection?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const NamiSegmentPicker: React.FC<Props> = ({
|
|
14
|
+
component,
|
|
15
|
+
scaleFactor,
|
|
16
|
+
onClose,
|
|
17
|
+
parentDirection,
|
|
18
|
+
}) => {
|
|
19
|
+
const containerStyle = useMemo(
|
|
20
|
+
() => applyStyles(component, scaleFactor, false, parentDirection),
|
|
21
|
+
[component, scaleFactor, parentDirection]
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const direction = component.direction ?? 'horizontal';
|
|
25
|
+
const parentFormId = component.formId ?? component.id ?? '';
|
|
26
|
+
const segments = component.components ?? [];
|
|
27
|
+
const spacing = parseSize(component.spacing, scaleFactor) ?? 0;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<View style={containerStyle}>
|
|
31
|
+
<ScrollView
|
|
32
|
+
horizontal={direction !== 'vertical'}
|
|
33
|
+
showsHorizontalScrollIndicator={false}
|
|
34
|
+
showsVerticalScrollIndicator={false}
|
|
35
|
+
contentContainerStyle={[
|
|
36
|
+
styles.scrollContent,
|
|
37
|
+
direction === 'vertical' ? styles.vertical : styles.horizontal,
|
|
38
|
+
]}
|
|
39
|
+
>
|
|
40
|
+
{segments.map((segment: any, i: number) => (
|
|
41
|
+
<View
|
|
42
|
+
key={segment.id ?? segment.value ?? i}
|
|
43
|
+
style={i === 0 ? null : { marginLeft: direction === 'horizontal' ? spacing : 0, marginTop: direction === 'vertical' ? spacing : 0 }}
|
|
44
|
+
>
|
|
45
|
+
<NamiSegmentPickerItem
|
|
46
|
+
component={segment}
|
|
47
|
+
scaleFactor={scaleFactor}
|
|
48
|
+
onClose={onClose}
|
|
49
|
+
parentDirection={direction}
|
|
50
|
+
parentFormId={parentFormId}
|
|
51
|
+
parentOnTap={component.onTap}
|
|
52
|
+
/>
|
|
53
|
+
</View>
|
|
54
|
+
))}
|
|
55
|
+
</ScrollView>
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const styles = StyleSheet.create({
|
|
61
|
+
scrollContent: {
|
|
62
|
+
alignItems: 'stretch',
|
|
63
|
+
flexGrow: 1,
|
|
64
|
+
},
|
|
65
|
+
horizontal: { flexDirection: 'row' },
|
|
66
|
+
vertical: { flexDirection: 'column' },
|
|
67
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
import { useFirstFocusReadyContext, usePaywallContext } from '../../context/PaywallContext';
|
|
4
|
+
import {
|
|
5
|
+
applyStyles,
|
|
6
|
+
childSpacingStyle,
|
|
7
|
+
extractPrefixedStyles,
|
|
8
|
+
parseColor,
|
|
9
|
+
parseSize,
|
|
10
|
+
shadowStyles,
|
|
11
|
+
textStyles,
|
|
12
|
+
} from '../../utils/styles';
|
|
13
|
+
import { handleAction } from '../../utils/actionHandler';
|
|
14
|
+
import { TemplateRenderer } from '../TemplateRenderer';
|
|
15
|
+
import type { TComponent } from '@namiml/sdk-core';
|
|
16
|
+
import { FocusedStyleProvider, useFocusableState, useFocusEnabled, useRegisterPreferredFocus } from '../../context/FocusContext';
|
|
17
|
+
import { useTVPreferredFocus } from '../../utils/tvFocus';
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
component: any;
|
|
21
|
+
scaleFactor: number;
|
|
22
|
+
onClose?: () => void;
|
|
23
|
+
parentDirection?: string;
|
|
24
|
+
parentFormId?: string;
|
|
25
|
+
parentOnTap?: { function: string; parameters?: Record<string, any> };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const NamiSegmentPickerItem: React.FC<Props> = ({
|
|
29
|
+
component,
|
|
30
|
+
scaleFactor,
|
|
31
|
+
onClose,
|
|
32
|
+
parentDirection,
|
|
33
|
+
parentFormId,
|
|
34
|
+
parentOnTap,
|
|
35
|
+
}) => {
|
|
36
|
+
const ctx = usePaywallContext();
|
|
37
|
+
const focusReadyCtx = useFirstFocusReadyContext();
|
|
38
|
+
const focusEnabled = useFocusEnabled();
|
|
39
|
+
const pressableRef = useRef<any>(null);
|
|
40
|
+
const { isFocused, onFocus, onBlur } = useFocusableState(
|
|
41
|
+
Boolean(component.focused || component.checked),
|
|
42
|
+
focusEnabled,
|
|
43
|
+
);
|
|
44
|
+
useTVPreferredFocus(
|
|
45
|
+
pressableRef,
|
|
46
|
+
focusEnabled && Boolean(component.focused || component.checked),
|
|
47
|
+
);
|
|
48
|
+
useRegisterPreferredFocus(
|
|
49
|
+
pressableRef.current,
|
|
50
|
+
focusEnabled && Boolean(component.focused || component.checked),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const selectedFormValue = parentFormId ? ctx.state.formStates?.[parentFormId] : '';
|
|
54
|
+
const currentGroupId = ctx.state.currentGroupId ?? '';
|
|
55
|
+
const isActive = component.formId
|
|
56
|
+
? (selectedFormValue ? selectedFormValue === component.formId : !!component.checked)
|
|
57
|
+
: (currentGroupId ? component.id === currentGroupId : !!component.checked);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!component.checked) return;
|
|
61
|
+
if (component.formId && parentFormId) {
|
|
62
|
+
if (!selectedFormValue) {
|
|
63
|
+
ctx.setFormState(parentFormId, component.formId);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!ctx.state.currentGroupId && component.id) {
|
|
68
|
+
ctx.setCurrentGroupData(component.id, component.text ?? '');
|
|
69
|
+
}
|
|
70
|
+
}, [
|
|
71
|
+
component.checked,
|
|
72
|
+
component.formId,
|
|
73
|
+
component.id,
|
|
74
|
+
component.text,
|
|
75
|
+
parentFormId,
|
|
76
|
+
selectedFormValue,
|
|
77
|
+
ctx,
|
|
78
|
+
ctx.state.currentGroupId,
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
const activeStyles = extractPrefixedStyles(component, 'active');
|
|
82
|
+
const inactiveStyles = extractPrefixedStyles(component, 'inactive');
|
|
83
|
+
const merged = { ...component, ...(isActive ? activeStyles : inactiveStyles) };
|
|
84
|
+
|
|
85
|
+
const itemStyle = useMemo(() => {
|
|
86
|
+
const base = applyStyles(merged, scaleFactor, isFocused, parentDirection);
|
|
87
|
+
const fill = parseColor(merged.fillColor ?? merged.selectedFillColor);
|
|
88
|
+
if (fill) base.backgroundColor = fill;
|
|
89
|
+
if (merged.borderRadius != null) {
|
|
90
|
+
base.borderRadius = parseSize(merged.borderRadius, scaleFactor);
|
|
91
|
+
}
|
|
92
|
+
if (merged.dropShadow) {
|
|
93
|
+
Object.assign(base, shadowStyles({ ...merged, dropShadow: merged.dropShadow } as any));
|
|
94
|
+
}
|
|
95
|
+
return base;
|
|
96
|
+
}, [merged, scaleFactor, parentDirection, isFocused]);
|
|
97
|
+
|
|
98
|
+
const onPress = () => {
|
|
99
|
+
if (component.formId && parentFormId) {
|
|
100
|
+
ctx.setFormState(parentFormId, component.formId);
|
|
101
|
+
} else if (component.id) {
|
|
102
|
+
ctx.setCurrentGroupData(component.id, component.text ?? '');
|
|
103
|
+
}
|
|
104
|
+
if (component.onTap) {
|
|
105
|
+
handleAction({
|
|
106
|
+
onTap: component.onTap,
|
|
107
|
+
ctx,
|
|
108
|
+
onClose,
|
|
109
|
+
componentChange: {
|
|
110
|
+
id: component.id,
|
|
111
|
+
name: component.title ?? component.text,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (parentOnTap) {
|
|
116
|
+
handleAction({
|
|
117
|
+
onTap: parentOnTap,
|
|
118
|
+
ctx,
|
|
119
|
+
onClose,
|
|
120
|
+
componentChange: {
|
|
121
|
+
id: component.id,
|
|
122
|
+
name: component.title ?? component.text,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const handleFocus = () => {
|
|
129
|
+
onFocus();
|
|
130
|
+
focusReadyCtx.notifyFirstFocusReady(
|
|
131
|
+
ctx.state.selectedPaywall?.id ?? 'unknown',
|
|
132
|
+
ctx.state.currentPage ?? 'unknown',
|
|
133
|
+
ctx.state.formFactor,
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const direction = component.direction ?? 'horizontal';
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<Pressable
|
|
141
|
+
ref={pressableRef}
|
|
142
|
+
onPress={onPress}
|
|
143
|
+
onFocus={handleFocus}
|
|
144
|
+
onBlur={onBlur}
|
|
145
|
+
hasTVPreferredFocus={focusEnabled && Boolean(component.focused || component.checked)}
|
|
146
|
+
style={({ pressed }) => [styles.item, itemStyle, pressed ? styles.pressed : null]}
|
|
147
|
+
>
|
|
148
|
+
<FocusedStyleProvider focused={isFocused || isActive}>
|
|
149
|
+
{component.components?.length ? (
|
|
150
|
+
component.components.map((child: TComponent, i: number) => (
|
|
151
|
+
<View
|
|
152
|
+
key={child.id ?? i}
|
|
153
|
+
style={i === 0 ? null : childSpacingStyle(i, { spacing: component.spacing, direction }, scaleFactor)}
|
|
154
|
+
>
|
|
155
|
+
<TemplateRenderer
|
|
156
|
+
component={child}
|
|
157
|
+
scaleFactor={scaleFactor}
|
|
158
|
+
onClose={onClose}
|
|
159
|
+
parentDirection={direction}
|
|
160
|
+
/>
|
|
161
|
+
</View>
|
|
162
|
+
))
|
|
163
|
+
) : (
|
|
164
|
+
<Text
|
|
165
|
+
style={[
|
|
166
|
+
textStyles(merged, scaleFactor, isFocused || isActive),
|
|
167
|
+
isActive && {
|
|
168
|
+
color: parseColor(activeStyles.fontColor ?? activeStyles.textColor ?? component.selectedFontColor) ?? '#fff',
|
|
169
|
+
},
|
|
170
|
+
]}
|
|
171
|
+
allowFontScaling={false}
|
|
172
|
+
>
|
|
173
|
+
{component.text ?? component.title ?? ''}
|
|
174
|
+
</Text>
|
|
175
|
+
)}
|
|
176
|
+
</FocusedStyleProvider>
|
|
177
|
+
</Pressable>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const styles = StyleSheet.create({
|
|
182
|
+
item: { minWidth: 0 },
|
|
183
|
+
pressed: { opacity: 0.7 },
|
|
184
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { parseSize, applyStyles } from '../../utils/styles';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
component: any;
|
|
7
|
+
scaleFactor: number;
|
|
8
|
+
parentDirection?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const NamiSpacer: React.FC<Props> = ({ component, scaleFactor, parentDirection }) => {
|
|
12
|
+
const hasExplicitSize = component.height || component.fixedHeight || component.width || component.fixedWidth;
|
|
13
|
+
const height = parseSize(component.height ?? component.fixedHeight ?? component.spacing ?? 8, scaleFactor);
|
|
14
|
+
const width = parseSize(component.width ?? component.fixedWidth, scaleFactor);
|
|
15
|
+
const baseStyle = applyStyles(component, scaleFactor, false, parentDirection);
|
|
16
|
+
const style = {
|
|
17
|
+
...baseStyle,
|
|
18
|
+
height,
|
|
19
|
+
width,
|
|
20
|
+
...(hasExplicitSize ? {} : { flexGrow: 1 }),
|
|
21
|
+
};
|
|
22
|
+
return <View style={style} />;
|
|
23
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
// import { Platform, Text, View } from 'react-native';
|
|
3
|
+
import { Text, View } from 'react-native';
|
|
4
|
+
import { applyStyles, textStyles } from '../../utils/styles';
|
|
5
|
+
import { useInheritedFocusedStyle } from '../../context/FocusContext';
|
|
6
|
+
import { iconPathsByName } from '../../utils/iconMap';
|
|
7
|
+
|
|
8
|
+
let SvgComponent: React.ComponentType<any> | null = null;
|
|
9
|
+
let SvgPathComponent: React.ComponentType<any> | null = null;
|
|
10
|
+
// if (!Platform.isTV) {
|
|
11
|
+
try {
|
|
12
|
+
const reactNativeSvg = require('react-native-svg');
|
|
13
|
+
SvgComponent = reactNativeSvg.default ?? reactNativeSvg.Svg ?? null;
|
|
14
|
+
SvgPathComponent = reactNativeSvg.Path ?? null;
|
|
15
|
+
} catch {
|
|
16
|
+
SvgComponent = null;
|
|
17
|
+
SvgPathComponent = null;
|
|
18
|
+
}
|
|
19
|
+
// }
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
component: any;
|
|
23
|
+
scaleFactor: number;
|
|
24
|
+
parentDirection?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SYMBOL_FALLBACK_MAP: Record<string, string> = {
|
|
28
|
+
check: '\u2713',
|
|
29
|
+
close: '\u2715',
|
|
30
|
+
plus: '+',
|
|
31
|
+
minus: '\u2212',
|
|
32
|
+
right: '\u203A',
|
|
33
|
+
left: '\u2039',
|
|
34
|
+
up: '\u2303',
|
|
35
|
+
down: '\u2304',
|
|
36
|
+
info: '\u2139',
|
|
37
|
+
question: '?',
|
|
38
|
+
play: '\u25B6',
|
|
39
|
+
pause: '\u23F8',
|
|
40
|
+
sound: '\uD83D\uDD0A',
|
|
41
|
+
volume: '\uD83D\uDD0A',
|
|
42
|
+
muted: '\uD83D\uDD07',
|
|
43
|
+
heart: '\u2665',
|
|
44
|
+
star: '\u2605',
|
|
45
|
+
like: '\uD83D\uDC4D',
|
|
46
|
+
unlock: '\uD83D\uDD13',
|
|
47
|
+
calendar: '\uD83D\uDCC5',
|
|
48
|
+
cloud: '\u2601',
|
|
49
|
+
fire: '\uD83D\uDD25',
|
|
50
|
+
bell: '\uD83D\uDD14',
|
|
51
|
+
message: '\uD83D\uDCAC',
|
|
52
|
+
smile: '\u263A',
|
|
53
|
+
caret: '\u25BE',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function resolveSymbol(name?: string): string {
|
|
57
|
+
if (!name) return '';
|
|
58
|
+
const key = name.toLowerCase();
|
|
59
|
+
const exact = SYMBOL_FALLBACK_MAP[key];
|
|
60
|
+
if (exact) return exact;
|
|
61
|
+
|
|
62
|
+
const match = Object.entries(SYMBOL_FALLBACK_MAP).find(([token]) => key.includes(token));
|
|
63
|
+
return match?.[1] ?? '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const NamiSymbol: React.FC<Props> = ({ component, scaleFactor, parentDirection }) => {
|
|
67
|
+
const isFocused = useInheritedFocusedStyle();
|
|
68
|
+
const containerStyle = applyStyles(component, scaleFactor, isFocused, parentDirection);
|
|
69
|
+
const symbolStyle = textStyles(component, scaleFactor, isFocused);
|
|
70
|
+
const svgPaths = useMemo(() => iconPathsByName(component.name), [component.name]);
|
|
71
|
+
const value = resolveSymbol(component.name) || (component.text ?? '');
|
|
72
|
+
const iconSize = typeof symbolStyle.fontSize === 'number' ? symbolStyle.fontSize : 16 * scaleFactor;
|
|
73
|
+
const iconColor = typeof symbolStyle.color === 'string' ? symbolStyle.color : '#ffffff';
|
|
74
|
+
const iconMarginTop = typeof symbolStyle.lineHeight === 'number'
|
|
75
|
+
? symbolStyle.lineHeight / 10
|
|
76
|
+
: iconSize * 0.12;
|
|
77
|
+
const canRenderSvg = Boolean(svgPaths && SvgComponent && SvgPathComponent);
|
|
78
|
+
const resolvedSvgPaths = svgPaths ?? [];
|
|
79
|
+
|
|
80
|
+
if (!svgPaths && !value) return null;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<View style={containerStyle}>
|
|
84
|
+
{canRenderSvg ? (
|
|
85
|
+
<View style={{ marginTop: iconMarginTop }}>
|
|
86
|
+
{(() => {
|
|
87
|
+
const SafeSvgComponent = SvgComponent as React.ComponentType<any>;
|
|
88
|
+
const SafeSvgPathComponent = SvgPathComponent as React.ComponentType<any>;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<SafeSvgComponent width={iconSize} height={iconSize} viewBox="64 64 896 896">
|
|
92
|
+
{resolvedSvgPaths.map((d, index) => (
|
|
93
|
+
<SafeSvgPathComponent key={`${component.name ?? 'icon'}-${index}`} d={d} fill={iconColor} />
|
|
94
|
+
))}
|
|
95
|
+
</SafeSvgComponent>
|
|
96
|
+
);
|
|
97
|
+
})()}
|
|
98
|
+
</View>
|
|
99
|
+
) : (
|
|
100
|
+
<Text style={symbolStyle} allowFontScaling={false}>{value}</Text>
|
|
101
|
+
)}
|
|
102
|
+
</View>
|
|
103
|
+
);
|
|
104
|
+
};
|