@umituz/react-native-subscription 2.11.26 → 2.11.28
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 +1 -1
- package/src/presentation/components/feedback/PaywallFeedbackModal.tsx +1 -2
- package/src/presentation/components/paywall/BestValueBadge.tsx +24 -19
- 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 -23
- 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/src/revenuecat/domain/value-objects/RevenueCatConfig.ts +3 -6
- package/src/revenuecat/infrastructure/utils/ApiKeyResolver.ts +3 -9
- package/src/utils/premiumUtils.ts +0 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.28",
|
|
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",
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
TouchableWithoutFeedback,
|
|
12
12
|
TextInput,
|
|
13
13
|
KeyboardAvoidingView,
|
|
14
|
-
Platform,
|
|
15
14
|
} from "react-native";
|
|
16
15
|
import { AtomicText } from "@umituz/react-native-design-system";
|
|
17
16
|
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
@@ -79,7 +78,7 @@ export const PaywallFeedbackModal: React.FC<PaywallFeedbackModalProps> = React.m
|
|
|
79
78
|
<TouchableWithoutFeedback onPress={handleSkip}>
|
|
80
79
|
<View style={styles.overlay}>
|
|
81
80
|
<KeyboardAvoidingView
|
|
82
|
-
behavior=
|
|
81
|
+
behavior="padding"
|
|
83
82
|
style={styles.keyboardView}
|
|
84
83
|
>
|
|
85
84
|
<TouchableWithoutFeedback>
|
|
@@ -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, { 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,22 +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
|
-
|
|
113
|
-
});
|
|
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
|
+
};
|
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
import type { CustomerInfo } from "react-native-purchases";
|
|
7
7
|
|
|
8
8
|
export interface RevenueCatConfig {
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
/** Android API key */
|
|
12
|
-
androidApiKey?: string;
|
|
9
|
+
/** Primary API key - resolved by main app based on platform */
|
|
10
|
+
apiKey?: string;
|
|
13
11
|
/** Test Store key for development/Expo Go testing */
|
|
14
12
|
testStoreKey?: string;
|
|
15
13
|
/** Entitlement identifier to check for premium status (REQUIRED - app specific) */
|
|
@@ -44,7 +42,6 @@ export interface RevenueCatConfig {
|
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
export interface RevenueCatConfigRequired {
|
|
47
|
-
|
|
48
|
-
androidApiKey: string;
|
|
45
|
+
apiKey: string;
|
|
49
46
|
}
|
|
50
47
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* API Key Resolver
|
|
3
3
|
* Resolves RevenueCat API key from configuration
|
|
4
|
+
* NOTE: Main app is responsible for resolving platform-specific keys
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import { Platform } from "react-native";
|
|
7
7
|
import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
|
|
8
8
|
import { isExpoGo, isProductionBuild } from "./ExpoGoDetector";
|
|
9
9
|
|
|
@@ -14,23 +14,21 @@ import { isExpoGo, isProductionBuild } from "./ExpoGoDetector";
|
|
|
14
14
|
export function shouldUseTestStore(config: RevenueCatConfig): boolean {
|
|
15
15
|
const testKey = config.testStoreKey;
|
|
16
16
|
|
|
17
|
-
// No test key configured - always use production keys
|
|
18
17
|
if (!testKey) {
|
|
19
18
|
return false;
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
// CRITICAL: Production builds should NEVER use test store
|
|
23
21
|
if (isProductionBuild() && !isExpoGo()) {
|
|
24
22
|
return false;
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
// Only use test store in Expo Go
|
|
28
25
|
return isExpoGo();
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
/**
|
|
32
29
|
* Get RevenueCat API key from config
|
|
33
30
|
* Returns Test Store key if in Expo Go environment ONLY
|
|
31
|
+
* Main app must provide resolved platform-specific apiKey in config
|
|
34
32
|
*/
|
|
35
33
|
export function resolveApiKey(config: RevenueCatConfig): string | null {
|
|
36
34
|
const useTestStore = shouldUseTestStore(config);
|
|
@@ -39,11 +37,7 @@ export function resolveApiKey(config: RevenueCatConfig): string | null {
|
|
|
39
37
|
return config.testStoreKey ?? null;
|
|
40
38
|
}
|
|
41
39
|
|
|
42
|
-
const key =
|
|
43
|
-
? config.iosApiKey
|
|
44
|
-
: Platform.OS === 'android'
|
|
45
|
-
? config.androidApiKey
|
|
46
|
-
: config.iosApiKey;
|
|
40
|
+
const key = config.apiKey;
|
|
47
41
|
|
|
48
42
|
if (!key || key === "" || key.includes("YOUR_")) {
|
|
49
43
|
return null;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Premium Utilities
|
|
3
|
-
*
|
|
4
|
-
* Re-export of premium status utilities for backward compatibility
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// Re-export for backward compatibility
|
|
8
|
-
export { getIsPremium } from './premiumStatusUtils';
|
|
9
|
-
export { getUserTierInfoAsync, checkPremiumAccessAsync } from './premiumAsyncUtils';
|