@umituz/react-native-subscription 2.11.27 → 2.12.0
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/package.json +2 -2
- package/src/presentation/components/paywall/BestValueBadge.tsx +24 -19
- package/src/presentation/components/paywall/CreditsTabContent.tsx +6 -13
- package/src/presentation/components/paywall/PaywallFeatureItem.tsx +22 -18
- package/src/presentation/components/paywall/PaywallFeaturesList.tsx +5 -1
- package/src/presentation/components/paywall/SubscriptionFooter.tsx +26 -22
- package/src/presentation/components/paywall/SubscriptionModal.tsx +23 -19
- package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +81 -11
- package/src/presentation/components/paywall/accordion/AccordionPlanCard.tsx +12 -8
- package/src/presentation/components/paywall/accordion/PlanCardDetails.tsx +24 -19
- package/src/presentation/components/paywall/accordion/PlanCardHeader.tsx +62 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "Complete subscription management with RevenueCat, paywall UI, and credits system for React Native apps",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@tanstack/react-query": "^5.0.0",
|
|
49
49
|
"@types/react": "~19.1.10",
|
|
50
|
-
"@umituz/react-native-design-system": "^2.
|
|
50
|
+
"@umituz/react-native-design-system": "^2.2.0",
|
|
51
51
|
"@umituz/react-native-firebase": "*",
|
|
52
52
|
"@umituz/react-native-legal": "*",
|
|
53
53
|
"@umituz/react-native-localization": "*",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
2
|
import { View, StyleSheet } from "react-native";
|
|
3
|
-
import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
3
|
+
import { AtomicText, useAppDesignTokens, useResponsive } from "@umituz/react-native-design-system";
|
|
4
4
|
import { LinearGradient } from "expo-linear-gradient";
|
|
5
5
|
|
|
6
6
|
interface BestValueBadgeProps {
|
|
@@ -11,6 +11,10 @@ interface BestValueBadgeProps {
|
|
|
11
11
|
export const BestValueBadge: React.FC<BestValueBadgeProps> = React.memo(
|
|
12
12
|
({ text, visible = true }) => {
|
|
13
13
|
const tokens = useAppDesignTokens();
|
|
14
|
+
const { spacingMultiplier, getFontSize } = useResponsive();
|
|
15
|
+
|
|
16
|
+
const styles = useMemo(() => createStyles(spacingMultiplier), [spacingMultiplier]);
|
|
17
|
+
const fontSize = getFontSize(10);
|
|
14
18
|
|
|
15
19
|
if (!visible) return null;
|
|
16
20
|
|
|
@@ -24,7 +28,7 @@ export const BestValueBadge: React.FC<BestValueBadgeProps> = React.memo(
|
|
|
24
28
|
>
|
|
25
29
|
<AtomicText
|
|
26
30
|
type="labelSmall"
|
|
27
|
-
style={{ color: tokens.colors.onPrimary, fontWeight: "800", textTransform: "uppercase", fontSize
|
|
31
|
+
style={{ color: tokens.colors.onPrimary, fontWeight: "800", textTransform: "uppercase", fontSize }}
|
|
28
32
|
>
|
|
29
33
|
{text}
|
|
30
34
|
</AtomicText>
|
|
@@ -36,19 +40,20 @@ export const BestValueBadge: React.FC<BestValueBadgeProps> = React.memo(
|
|
|
36
40
|
|
|
37
41
|
BestValueBadge.displayName = "BestValueBadge";
|
|
38
42
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
43
|
+
const createStyles = (spacingMult: number) =>
|
|
44
|
+
StyleSheet.create({
|
|
45
|
+
badgeContainer: {
|
|
46
|
+
position: "absolute",
|
|
47
|
+
top: -12 * spacingMult,
|
|
48
|
+
right: 16 * spacingMult,
|
|
49
|
+
zIndex: 1,
|
|
50
|
+
alignSelf: "flex-end",
|
|
51
|
+
},
|
|
52
|
+
badge: {
|
|
53
|
+
paddingHorizontal: 16 * spacingMult,
|
|
54
|
+
paddingVertical: 6 * spacingMult,
|
|
55
|
+
borderRadius: 16 * spacingMult,
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
justifyContent: "center",
|
|
58
|
+
},
|
|
59
|
+
});
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { View, StyleSheet, ScrollView } from "react-native";
|
|
8
|
-
import { AtomicText, AtomicButton } from "@umituz/react-native-design-system";
|
|
9
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
8
|
+
import { AtomicText, AtomicButton, Grid, useResponsiveDesignTokens } from "@umituz/react-native-design-system";
|
|
10
9
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
11
10
|
import { CreditsPackageCard } from "./CreditsPackageCard";
|
|
12
11
|
import { PaywallLegalFooter } from "./PaywallLegalFooter";
|
|
@@ -38,7 +37,7 @@ export const CreditsTabContent: React.FC<CreditsTabContentProps> = React.memo(
|
|
|
38
37
|
creditsInfoText,
|
|
39
38
|
processingText,
|
|
40
39
|
}) => {
|
|
41
|
-
const tokens =
|
|
40
|
+
const tokens = useResponsiveDesignTokens();
|
|
42
41
|
const { t } = useLocalization();
|
|
43
42
|
|
|
44
43
|
const needsCredits = requiredCredits && requiredCredits > currentCredits;
|
|
@@ -72,10 +71,10 @@ export const CreditsTabContent: React.FC<CreditsTabContentProps> = React.memo(
|
|
|
72
71
|
|
|
73
72
|
<ScrollView
|
|
74
73
|
style={styles.scrollView}
|
|
75
|
-
contentContainerStyle={styles.scrollContent}
|
|
74
|
+
contentContainerStyle={[styles.scrollContent, { paddingHorizontal: tokens.spacing.lg, paddingBottom: tokens.spacing.md }]}
|
|
76
75
|
showsVerticalScrollIndicator={false}
|
|
77
76
|
>
|
|
78
|
-
<
|
|
77
|
+
<Grid mobileColumns={1} tabletColumns={2} gap={tokens.spacing.sm}>
|
|
79
78
|
{packages.map((pkg) => (
|
|
80
79
|
<CreditsPackageCard
|
|
81
80
|
key={pkg.id}
|
|
@@ -84,7 +83,7 @@ export const CreditsTabContent: React.FC<CreditsTabContentProps> = React.memo(
|
|
|
84
83
|
onSelect={() => onSelectPackage(pkg.id)}
|
|
85
84
|
/>
|
|
86
85
|
))}
|
|
87
|
-
</
|
|
86
|
+
</Grid>
|
|
88
87
|
</ScrollView>
|
|
89
88
|
|
|
90
89
|
<View style={styles.footer}>
|
|
@@ -116,13 +115,7 @@ const styles = StyleSheet.create({
|
|
|
116
115
|
scrollView: {
|
|
117
116
|
flex: 1,
|
|
118
117
|
},
|
|
119
|
-
scrollContent: {
|
|
120
|
-
paddingHorizontal: 24,
|
|
121
|
-
paddingBottom: 16,
|
|
122
|
-
},
|
|
123
|
-
packagesContainer: {
|
|
124
|
-
gap: 12,
|
|
125
|
-
},
|
|
118
|
+
scrollContent: {},
|
|
126
119
|
footer: {
|
|
127
120
|
padding: 24,
|
|
128
121
|
paddingTop: 16,
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import React, { useMemo } from "react";
|
|
7
7
|
import { View, StyleSheet } from "react-native";
|
|
8
|
-
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system";
|
|
9
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
8
|
+
import { AtomicText, AtomicIcon, useAppDesignTokens, useResponsive } from "@umituz/react-native-design-system";
|
|
10
9
|
|
|
11
10
|
interface PaywallFeatureItemProps {
|
|
12
11
|
icon: string;
|
|
@@ -16,6 +15,12 @@ interface PaywallFeatureItemProps {
|
|
|
16
15
|
export const PaywallFeatureItem: React.FC<PaywallFeatureItemProps> = React.memo(
|
|
17
16
|
({ icon, text }) => {
|
|
18
17
|
const tokens = useAppDesignTokens();
|
|
18
|
+
const { spacingMultiplier, getFontSize } = useResponsive();
|
|
19
|
+
|
|
20
|
+
const styles = useMemo(() => createStyles(spacingMultiplier), [spacingMultiplier]);
|
|
21
|
+
const fontSize = getFontSize(15);
|
|
22
|
+
const lineHeight = getFontSize(22);
|
|
23
|
+
const iconSize = getFontSize(20);
|
|
19
24
|
|
|
20
25
|
// Pass icon name directly to AtomicIcon (which uses Ionicons)
|
|
21
26
|
// Do NOT capitalize, as Ionicons names are lowercase/kebab-case.
|
|
@@ -28,13 +33,13 @@ export const PaywallFeatureItem: React.FC<PaywallFeatureItemProps> = React.memo(
|
|
|
28
33
|
<View style={styles.featureItem}>
|
|
29
34
|
<AtomicIcon
|
|
30
35
|
name={iconName}
|
|
31
|
-
customSize={
|
|
36
|
+
customSize={iconSize}
|
|
32
37
|
customColor={tokens.colors.primary}
|
|
33
38
|
style={styles.featureIcon}
|
|
34
39
|
/>
|
|
35
40
|
<AtomicText
|
|
36
41
|
type="bodyMedium"
|
|
37
|
-
style={[styles.featureText, { color: tokens.colors.textPrimary }]}
|
|
42
|
+
style={[styles.featureText, { color: tokens.colors.textPrimary, fontSize, lineHeight }]}
|
|
38
43
|
>
|
|
39
44
|
{text}
|
|
40
45
|
</AtomicText>
|
|
@@ -45,17 +50,16 @@ export const PaywallFeatureItem: React.FC<PaywallFeatureItemProps> = React.memo(
|
|
|
45
50
|
|
|
46
51
|
PaywallFeatureItem.displayName = "PaywallFeatureItem";
|
|
47
52
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
});
|
|
53
|
+
const createStyles = (spacingMult: number) =>
|
|
54
|
+
StyleSheet.create({
|
|
55
|
+
featureItem: {
|
|
56
|
+
flexDirection: "row",
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
},
|
|
59
|
+
featureIcon: {
|
|
60
|
+
marginRight: 12 * spacingMult,
|
|
61
|
+
},
|
|
62
|
+
featureText: {
|
|
63
|
+
flex: 1,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { useResponsive } from "@umituz/react-native-design-system";
|
|
8
9
|
import { PaywallFeatureItem } from "./PaywallFeatureItem";
|
|
9
10
|
|
|
10
11
|
interface PaywallFeaturesListProps {
|
|
@@ -18,12 +19,15 @@ interface PaywallFeaturesListProps {
|
|
|
18
19
|
|
|
19
20
|
export const PaywallFeaturesList: React.FC<PaywallFeaturesListProps> = React.memo(
|
|
20
21
|
({ features, containerStyle, gap = 12 }) => {
|
|
22
|
+
const { spacingMultiplier } = useResponsive();
|
|
23
|
+
const responsiveGap = gap * spacingMultiplier;
|
|
24
|
+
|
|
21
25
|
return (
|
|
22
26
|
<View style={[styles.container, containerStyle]}>
|
|
23
27
|
{features.map((feature, index) => (
|
|
24
28
|
<View
|
|
25
29
|
key={`${feature.icon}-${feature.text}-${index}`}
|
|
26
|
-
style={{ marginBottom: index < features.length - 1 ?
|
|
30
|
+
style={{ marginBottom: index < features.length - 1 ? responsiveGap : 0 }}
|
|
27
31
|
>
|
|
28
32
|
<PaywallFeatureItem icon={feature.icon} text={feature.text} />
|
|
29
33
|
</View>
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
2
|
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
3
3
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
4
|
-
import { AtomicText } from "@umituz/react-native-design-system";
|
|
5
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
4
|
+
import { AtomicText, useAppDesignTokens, useResponsive } from "@umituz/react-native-design-system";
|
|
6
5
|
import { PaywallLegalFooter } from "./PaywallLegalFooter";
|
|
7
6
|
|
|
8
7
|
interface SubscriptionFooterProps {
|
|
@@ -42,6 +41,10 @@ export const SubscriptionFooter: React.FC<SubscriptionFooterProps> = React.memo(
|
|
|
42
41
|
onRestore,
|
|
43
42
|
}) => {
|
|
44
43
|
const tokens = useAppDesignTokens();
|
|
44
|
+
const { spacingMultiplier, getFontSize } = useResponsive();
|
|
45
|
+
|
|
46
|
+
const styles = useMemo(() => createStyles(spacingMultiplier), [spacingMultiplier]);
|
|
47
|
+
const buttonFontSize = getFontSize(16);
|
|
45
48
|
|
|
46
49
|
const isDisabled = !selectedPkg || isProcessing || isLoading;
|
|
47
50
|
|
|
@@ -65,7 +68,7 @@ export const SubscriptionFooter: React.FC<SubscriptionFooterProps> = React.memo(
|
|
|
65
68
|
style={{
|
|
66
69
|
color: tokens.colors.onPrimary,
|
|
67
70
|
fontWeight: "800",
|
|
68
|
-
fontSize:
|
|
71
|
+
fontSize: buttonFontSize,
|
|
69
72
|
}}
|
|
70
73
|
>
|
|
71
74
|
{isProcessing ? processingText : purchaseButtonText}
|
|
@@ -92,21 +95,22 @@ export const SubscriptionFooter: React.FC<SubscriptionFooterProps> = React.memo(
|
|
|
92
95
|
|
|
93
96
|
SubscriptionFooter.displayName = "SubscriptionFooter";
|
|
94
97
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
98
|
+
const createStyles = (spacingMult: number) =>
|
|
99
|
+
StyleSheet.create({
|
|
100
|
+
container: {},
|
|
101
|
+
actions: {
|
|
102
|
+
paddingHorizontal: 24 * spacingMult,
|
|
103
|
+
paddingVertical: 16 * spacingMult,
|
|
104
|
+
gap: 12 * spacingMult,
|
|
105
|
+
},
|
|
106
|
+
gradientButton: {
|
|
107
|
+
paddingVertical: 16 * spacingMult,
|
|
108
|
+
borderRadius: 16 * spacingMult,
|
|
109
|
+
alignItems: "center",
|
|
110
|
+
justifyContent: "center",
|
|
111
|
+
},
|
|
112
|
+
restoreButton: {
|
|
113
|
+
alignItems: "center",
|
|
114
|
+
paddingVertical: 8 * spacingMult,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Fullscreen subscription flow using BaseModal from design system
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React from "react";
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
7
|
import { View, StyleSheet, ScrollView } from "react-native";
|
|
8
|
-
import { BaseModal } from "@umituz/react-native-design-system";
|
|
8
|
+
import { BaseModal, useResponsive } from "@umituz/react-native-design-system";
|
|
9
9
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
10
10
|
|
|
11
11
|
import { SubscriptionModalHeader } from "./SubscriptionModalHeader";
|
|
@@ -85,6 +85,9 @@ export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo((p
|
|
|
85
85
|
onClose,
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
const { spacingMultiplier } = useResponsive();
|
|
89
|
+
const styles = useMemo(() => createStyles(spacingMultiplier), [spacingMultiplier]);
|
|
90
|
+
|
|
88
91
|
return (
|
|
89
92
|
<BaseModal visible={visible} onClose={onClose}>
|
|
90
93
|
<View style={styles.container}>
|
|
@@ -145,20 +148,21 @@ export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo((p
|
|
|
145
148
|
|
|
146
149
|
SubscriptionModal.displayName = "SubscriptionModal";
|
|
147
150
|
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
151
|
+
const createStyles = (spacingMult: number) =>
|
|
152
|
+
StyleSheet.create({
|
|
153
|
+
container: {
|
|
154
|
+
flex: 1,
|
|
155
|
+
width: "100%",
|
|
156
|
+
},
|
|
157
|
+
scrollView: {
|
|
158
|
+
flex: 1,
|
|
159
|
+
},
|
|
160
|
+
scrollContent: {
|
|
161
|
+
flexGrow: 1,
|
|
162
|
+
paddingBottom: 32 * spacingMult,
|
|
163
|
+
},
|
|
164
|
+
featuresList: {
|
|
165
|
+
paddingHorizontal: 24 * spacingMult,
|
|
166
|
+
marginBottom: 24 * spacingMult,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
@@ -3,24 +3,91 @@
|
|
|
3
3
|
* Single Responsibility: Display a subscription plan option
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React from "react";
|
|
7
|
-
import { View, TouchableOpacity } from "react-native";
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
8
|
import { AtomicText } from "@umituz/react-native-design-system";
|
|
9
|
-
import { useAppDesignTokens, withAlpha } from "@umituz/react-native-design-system";
|
|
9
|
+
import { useAppDesignTokens, withAlpha, useResponsive } from "@umituz/react-native-design-system";
|
|
10
10
|
import { formatPrice } from "../../../utils/priceUtils";
|
|
11
11
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
12
12
|
import { BestValueBadge } from "./BestValueBadge";
|
|
13
13
|
import { getPeriodLabel, isYearlyPackage } from "../../../utils/packagePeriodUtils";
|
|
14
14
|
import { LinearGradient } from "expo-linear-gradient";
|
|
15
15
|
import type { SubscriptionPlanCardProps } from "./SubscriptionPlanCardTypes";
|
|
16
|
-
import { styles } from "./SubscriptionPlanCardStyles";
|
|
17
16
|
|
|
18
17
|
export type { SubscriptionPlanCardProps } from "./SubscriptionPlanCardTypes";
|
|
19
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Create responsive styles for subscription plan card
|
|
21
|
+
*/
|
|
22
|
+
const createStyles = (spacingMult: number, touchTarget: number) => {
|
|
23
|
+
const basePadding = 18;
|
|
24
|
+
const baseRadius = 16;
|
|
25
|
+
const baseCreditRadius = 12;
|
|
26
|
+
|
|
27
|
+
const radioSize = Math.max(touchTarget * 0.5, 24);
|
|
28
|
+
const radioInnerSize = radioSize * 0.5;
|
|
29
|
+
|
|
30
|
+
return StyleSheet.create({
|
|
31
|
+
container: {
|
|
32
|
+
borderRadius: baseRadius * spacingMult,
|
|
33
|
+
position: "relative",
|
|
34
|
+
overflow: "hidden",
|
|
35
|
+
},
|
|
36
|
+
gradientWrapper: {
|
|
37
|
+
flex: 1,
|
|
38
|
+
padding: basePadding * spacingMult,
|
|
39
|
+
},
|
|
40
|
+
content: {
|
|
41
|
+
flexDirection: "row",
|
|
42
|
+
justifyContent: "space-between",
|
|
43
|
+
alignItems: "center",
|
|
44
|
+
},
|
|
45
|
+
leftSection: {
|
|
46
|
+
flexDirection: "row",
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
flex: 1,
|
|
49
|
+
},
|
|
50
|
+
radio: {
|
|
51
|
+
width: radioSize,
|
|
52
|
+
height: radioSize,
|
|
53
|
+
borderRadius: radioSize / 2,
|
|
54
|
+
borderWidth: 2,
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
justifyContent: "center",
|
|
57
|
+
marginRight: 16 * spacingMult,
|
|
58
|
+
},
|
|
59
|
+
radioInner: {
|
|
60
|
+
width: radioInnerSize,
|
|
61
|
+
height: radioInnerSize,
|
|
62
|
+
borderRadius: radioInnerSize / 2,
|
|
63
|
+
},
|
|
64
|
+
textContainer: {
|
|
65
|
+
flex: 1,
|
|
66
|
+
},
|
|
67
|
+
title: {
|
|
68
|
+
fontWeight: "600",
|
|
69
|
+
marginBottom: 2 * spacingMult,
|
|
70
|
+
},
|
|
71
|
+
creditBadge: {
|
|
72
|
+
paddingHorizontal: 10 * spacingMult,
|
|
73
|
+
paddingVertical: 4 * spacingMult,
|
|
74
|
+
borderRadius: baseCreditRadius * spacingMult,
|
|
75
|
+
marginBottom: 4 * spacingMult,
|
|
76
|
+
},
|
|
77
|
+
rightSection: {
|
|
78
|
+
alignItems: "flex-end",
|
|
79
|
+
},
|
|
80
|
+
price: {
|
|
81
|
+
fontWeight: "700",
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
20
86
|
export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
21
87
|
React.memo(({ package: pkg, isSelected, onSelect, isBestValue = false, creditAmount }) => {
|
|
22
88
|
const tokens = useAppDesignTokens();
|
|
23
89
|
const { t } = useLocalization();
|
|
90
|
+
const { spacingMultiplier, getFontSize, minTouchTarget } = useResponsive();
|
|
24
91
|
|
|
25
92
|
const period = pkg.product.subscriptionPeriod;
|
|
26
93
|
const isYearly = isYearlyPackage(pkg);
|
|
@@ -41,6 +108,11 @@ export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
|
41
108
|
}
|
|
42
109
|
: {};
|
|
43
110
|
|
|
111
|
+
// Responsive styles
|
|
112
|
+
const styles = useMemo(() => createStyles(spacingMultiplier, minTouchTarget), [spacingMultiplier, minTouchTarget]);
|
|
113
|
+
const secondaryFontSize = getFontSize(11);
|
|
114
|
+
const creditFontSize = getFontSize(11);
|
|
115
|
+
|
|
44
116
|
return (
|
|
45
117
|
<TouchableOpacity
|
|
46
118
|
onPress={onSelect}
|
|
@@ -87,12 +159,12 @@ export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
|
87
159
|
>
|
|
88
160
|
{title}
|
|
89
161
|
</AtomicText>
|
|
90
|
-
{isYearly && (
|
|
162
|
+
{isYearly && monthlyEquivalent && (
|
|
91
163
|
<AtomicText
|
|
92
164
|
type="bodySmall"
|
|
93
|
-
style={{ color: tokens.colors.textSecondary, fontSize:
|
|
165
|
+
style={{ color: tokens.colors.textSecondary, fontSize: secondaryFontSize }}
|
|
94
166
|
>
|
|
95
|
-
{
|
|
167
|
+
{monthlyEquivalent}/mo
|
|
96
168
|
</AtomicText>
|
|
97
169
|
)}
|
|
98
170
|
</View>
|
|
@@ -117,7 +189,7 @@ export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
|
117
189
|
style={{
|
|
118
190
|
color: tokens.colors.primary,
|
|
119
191
|
fontWeight: "800",
|
|
120
|
-
fontSize:
|
|
192
|
+
fontSize: creditFontSize,
|
|
121
193
|
}}
|
|
122
194
|
>
|
|
123
195
|
{creditAmount} {t("paywall.credits") || "Credits"}
|
|
@@ -128,9 +200,7 @@ export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
|
128
200
|
type="titleMedium"
|
|
129
201
|
style={[styles.price, { color: tokens.colors.textPrimary }]}
|
|
130
202
|
>
|
|
131
|
-
{
|
|
132
|
-
? `${monthlyEquivalent}/mo`
|
|
133
|
-
: price}
|
|
203
|
+
{price}
|
|
134
204
|
</AtomicText>
|
|
135
205
|
</View>
|
|
136
206
|
</View>
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Expandable subscription plan card with credit display
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, { useCallback } from "react";
|
|
6
|
+
import React, { useCallback, useMemo } from "react";
|
|
7
7
|
import { View, StyleSheet, type StyleProp, type ViewStyle } from "react-native";
|
|
8
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
8
|
+
import { useAppDesignTokens, useResponsive } from "@umituz/react-native-design-system";
|
|
9
9
|
import { formatPrice } from "../../../../utils/priceUtils";
|
|
10
10
|
import { getPeriodLabel, isYearlyPackage } from "../../../../utils/packagePeriodUtils";
|
|
11
11
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
@@ -28,6 +28,9 @@ export const AccordionPlanCard: React.FC<AccordionPlanCardProps> = React.memo(
|
|
|
28
28
|
}) => {
|
|
29
29
|
const tokens = useAppDesignTokens();
|
|
30
30
|
const { t } = useLocalization();
|
|
31
|
+
const { spacingMultiplier } = useResponsive();
|
|
32
|
+
|
|
33
|
+
const styles = useMemo(() => createStyles(spacingMultiplier), [spacingMultiplier]);
|
|
31
34
|
|
|
32
35
|
const period = pkg.product.subscriptionPeriod;
|
|
33
36
|
const isYearly = isYearlyPackage(pkg);
|
|
@@ -88,9 +91,10 @@ export const AccordionPlanCard: React.FC<AccordionPlanCardProps> = React.memo(
|
|
|
88
91
|
|
|
89
92
|
AccordionPlanCard.displayName = "AccordionPlanCard";
|
|
90
93
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
94
|
+
const createStyles = (spacingMult: number) =>
|
|
95
|
+
StyleSheet.create({
|
|
96
|
+
container: {
|
|
97
|
+
borderRadius: 16 * spacingMult,
|
|
98
|
+
marginBottom: 12 * spacingMult,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* Expanded state of accordion subscription card
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React from "react";
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
7
|
import { View, StyleSheet } from "react-native";
|
|
8
8
|
import {
|
|
9
9
|
AtomicText,
|
|
10
10
|
useAppDesignTokens,
|
|
11
|
+
useResponsive,
|
|
11
12
|
} from "@umituz/react-native-design-system";
|
|
12
13
|
import type { PlanCardDetailsProps } from "./AccordionPlanCardTypes";
|
|
13
14
|
|
|
@@ -21,6 +22,9 @@ export const PlanCardDetails: React.FC<PlanCardDetailsProps> = ({
|
|
|
21
22
|
perMonthLabel = "Per Month",
|
|
22
23
|
}) => {
|
|
23
24
|
const tokens = useAppDesignTokens();
|
|
25
|
+
const { spacingMultiplier } = useResponsive();
|
|
26
|
+
|
|
27
|
+
const styles = useMemo(() => createStyles(spacingMultiplier), [spacingMultiplier]);
|
|
24
28
|
|
|
25
29
|
return (
|
|
26
30
|
<View
|
|
@@ -57,7 +61,10 @@ export const PlanCardDetails: React.FC<PlanCardDetailsProps> = ({
|
|
|
57
61
|
</AtomicText>
|
|
58
62
|
<AtomicText
|
|
59
63
|
type="bodyMedium"
|
|
60
|
-
style={{
|
|
64
|
+
style={{
|
|
65
|
+
color: tokens.colors.primary,
|
|
66
|
+
fontWeight: "700",
|
|
67
|
+
}}
|
|
61
68
|
>
|
|
62
69
|
{fullPrice}
|
|
63
70
|
</AtomicText>
|
|
@@ -74,10 +81,7 @@ export const PlanCardDetails: React.FC<PlanCardDetailsProps> = ({
|
|
|
74
81
|
</AtomicText>
|
|
75
82
|
<AtomicText
|
|
76
83
|
type="bodyMedium"
|
|
77
|
-
style={{
|
|
78
|
-
color: tokens.colors.primary,
|
|
79
|
-
fontWeight: "700",
|
|
80
|
-
}}
|
|
84
|
+
style={{ color: tokens.colors.textPrimary, fontWeight: "600" }}
|
|
81
85
|
>
|
|
82
86
|
{monthlyEquivalent}
|
|
83
87
|
</AtomicText>
|
|
@@ -87,16 +91,17 @@ export const PlanCardDetails: React.FC<PlanCardDetailsProps> = ({
|
|
|
87
91
|
);
|
|
88
92
|
};
|
|
89
93
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
94
|
+
const createStyles = (spacingMult: number) =>
|
|
95
|
+
StyleSheet.create({
|
|
96
|
+
container: {
|
|
97
|
+
paddingHorizontal: 16 * spacingMult,
|
|
98
|
+
paddingVertical: 12 * spacingMult,
|
|
99
|
+
borderTopWidth: 1,
|
|
100
|
+
gap: 10 * spacingMult,
|
|
101
|
+
},
|
|
102
|
+
row: {
|
|
103
|
+
flexDirection: "row",
|
|
104
|
+
justifyContent: "space-between",
|
|
105
|
+
alignItems: "center",
|
|
106
|
+
},
|
|
107
|
+
});
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* Collapsed state of accordion subscription card
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React from "react";
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
7
|
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
8
|
import {
|
|
9
9
|
AtomicText,
|
|
10
10
|
useAppDesignTokens,
|
|
11
11
|
withAlpha,
|
|
12
|
+
useResponsive,
|
|
12
13
|
} from "@umituz/react-native-design-system";
|
|
13
14
|
import { BestValueBadge } from "../BestValueBadge";
|
|
14
15
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
@@ -24,6 +25,13 @@ export const PlanCardHeader: React.FC<PlanCardHeaderProps> = ({
|
|
|
24
25
|
}) => {
|
|
25
26
|
const tokens = useAppDesignTokens();
|
|
26
27
|
const { t } = useLocalization();
|
|
28
|
+
const { spacingMultiplier, getFontSize, minTouchTarget } = useResponsive();
|
|
29
|
+
|
|
30
|
+
const styles = useMemo(
|
|
31
|
+
() => createStyles(spacingMultiplier, minTouchTarget),
|
|
32
|
+
[spacingMultiplier, minTouchTarget]
|
|
33
|
+
);
|
|
34
|
+
const creditFontSize = getFontSize(11);
|
|
27
35
|
|
|
28
36
|
return (
|
|
29
37
|
<TouchableOpacity
|
|
@@ -77,7 +85,7 @@ export const PlanCardHeader: React.FC<PlanCardHeaderProps> = ({
|
|
|
77
85
|
style={{
|
|
78
86
|
color: tokens.colors.primary,
|
|
79
87
|
fontWeight: "700",
|
|
80
|
-
fontSize:
|
|
88
|
+
fontSize: creditFontSize,
|
|
81
89
|
}}
|
|
82
90
|
>
|
|
83
91
|
{creditAmount} {t("paywall.credits") || "Credits"}
|
|
@@ -103,50 +111,55 @@ export const PlanCardHeader: React.FC<PlanCardHeaderProps> = ({
|
|
|
103
111
|
);
|
|
104
112
|
};
|
|
105
113
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
114
|
+
const createStyles = (spacingMult: number, touchTarget: number) => {
|
|
115
|
+
const radioSize = Math.max(touchTarget * 0.4, 22);
|
|
116
|
+
const radioInnerSize = radioSize * 0.55;
|
|
117
|
+
|
|
118
|
+
return StyleSheet.create({
|
|
119
|
+
container: {
|
|
120
|
+
width: "100%",
|
|
121
|
+
},
|
|
122
|
+
content: {
|
|
123
|
+
flexDirection: "row",
|
|
124
|
+
alignItems: "center",
|
|
125
|
+
justifyContent: "space-between",
|
|
126
|
+
paddingVertical: 16 * spacingMult,
|
|
127
|
+
paddingHorizontal: 16 * spacingMult,
|
|
128
|
+
},
|
|
129
|
+
leftSection: {
|
|
130
|
+
flexDirection: "row",
|
|
131
|
+
alignItems: "center",
|
|
132
|
+
flex: 1,
|
|
133
|
+
},
|
|
134
|
+
radio: {
|
|
135
|
+
width: radioSize,
|
|
136
|
+
height: radioSize,
|
|
137
|
+
borderRadius: radioSize / 2,
|
|
138
|
+
borderWidth: 2,
|
|
139
|
+
alignItems: "center",
|
|
140
|
+
justifyContent: "center",
|
|
141
|
+
marginRight: 12 * spacingMult,
|
|
142
|
+
},
|
|
143
|
+
radioInner: {
|
|
144
|
+
width: radioInnerSize,
|
|
145
|
+
height: radioInnerSize,
|
|
146
|
+
borderRadius: radioInnerSize / 2,
|
|
147
|
+
},
|
|
148
|
+
textContainer: {
|
|
149
|
+
flex: 1,
|
|
150
|
+
gap: 6 * spacingMult,
|
|
151
|
+
},
|
|
152
|
+
creditBadge: {
|
|
153
|
+
paddingHorizontal: 10 * spacingMult,
|
|
154
|
+
paddingVertical: 4 * spacingMult,
|
|
155
|
+
borderRadius: 12 * spacingMult,
|
|
156
|
+
borderWidth: 1,
|
|
157
|
+
alignSelf: "flex-start",
|
|
158
|
+
},
|
|
159
|
+
rightSection: {
|
|
160
|
+
flexDirection: "row",
|
|
161
|
+
alignItems: "center",
|
|
162
|
+
gap: 12 * spacingMult,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
};
|