@umituz/react-native-subscription 2.12.2 → 2.12.3
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/domains/index.ts +5 -0
- package/src/domains/paywall/components/CreditCard.tsx +117 -0
- package/src/domains/paywall/components/FeatureItem.tsx +50 -0
- package/src/domains/paywall/components/FeatureList.tsx +34 -0
- package/src/domains/paywall/components/PaywallFooter.tsx +98 -0
- package/src/{presentation/components/paywall/PaywallHeroHeader.tsx → domains/paywall/components/PaywallHeader.tsx} +15 -44
- package/src/domains/paywall/components/PaywallModal.tsx +187 -0
- package/src/domains/paywall/components/PaywallTabBar.tsx +102 -0
- package/src/domains/paywall/components/PlanCard.tsx +124 -0
- package/src/domains/paywall/components/index.ts +14 -0
- package/src/domains/paywall/entities/index.ts +5 -0
- package/src/domains/paywall/entities/types.ts +48 -0
- package/src/domains/paywall/hooks/index.ts +6 -0
- package/src/{presentation → domains/paywall}/hooks/usePaywall.ts +1 -1
- package/src/domains/paywall/index.ts +13 -0
- package/src/index.ts +15 -22
- package/src/domain/entities/paywall/CreditsPackage.ts +0 -16
- package/src/domain/entities/paywall/PaywallMode.ts +0 -6
- package/src/domain/entities/paywall/PaywallTab.ts +0 -11
- package/src/domain/entities/paywall/SubscriptionPlan.ts +0 -27
- package/src/presentation/components/paywall/BestValueBadge.tsx +0 -59
- package/src/presentation/components/paywall/CreditsPackageCard.tsx +0 -161
- package/src/presentation/components/paywall/CreditsTabContent.tsx +0 -123
- package/src/presentation/components/paywall/PaywallFeatureItem.tsx +0 -79
- package/src/presentation/components/paywall/PaywallFeaturesList.tsx +0 -47
- package/src/presentation/components/paywall/PaywallHeader.tsx +0 -82
- package/src/presentation/components/paywall/PaywallLegalFooter.tsx +0 -145
- package/src/presentation/components/paywall/PaywallLegalFooterStyles.ts +0 -53
- package/src/presentation/components/paywall/PaywallLegalFooterTypes.ts +0 -19
- package/src/presentation/components/paywall/PaywallModal.tsx +0 -162
- package/src/presentation/components/paywall/PaywallTabBar.tsx +0 -120
- package/src/presentation/components/paywall/SubscriptionFooter.tsx +0 -116
- package/src/presentation/components/paywall/SubscriptionModal.tsx +0 -168
- package/src/presentation/components/paywall/SubscriptionModalHeader.tsx +0 -78
- package/src/presentation/components/paywall/SubscriptionPackageList.tsx +0 -171
- package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +0 -213
- package/src/presentation/components/paywall/SubscriptionPlanCardStyles.ts +0 -61
- package/src/presentation/components/paywall/SubscriptionPlanCardTypes.ts +0 -15
- package/src/presentation/components/paywall/SubscriptionTabContent.tsx +0 -139
- package/src/presentation/components/paywall/accordion/AccordionPlanCard.tsx +0 -98
- package/src/presentation/components/paywall/accordion/AccordionPlanCardTypes.ts +0 -39
- package/src/presentation/components/paywall/accordion/PlanCardDetails.tsx +0 -107
- package/src/presentation/components/paywall/accordion/PlanCardHeader.tsx +0 -155
- package/src/presentation/components/paywall/accordion/index.ts +0 -12
- /package/src/{presentation → domains/paywall}/hooks/useSubscriptionModal.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.3",
|
|
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",
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit Card
|
|
3
|
+
* Credit package selection card
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicText, AtomicIcon, AtomicBadge, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
import type { CreditsPackage } from "../entities";
|
|
10
|
+
|
|
11
|
+
interface CreditCardProps {
|
|
12
|
+
pkg: CreditsPackage;
|
|
13
|
+
isSelected: boolean;
|
|
14
|
+
onSelect: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const CreditCard: React.FC<CreditCardProps> = React.memo(({ pkg, isSelected, onSelect }) => {
|
|
18
|
+
const tokens = useAppDesignTokens();
|
|
19
|
+
const totalCredits = pkg.credits + (pkg.bonus ?? 0);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<TouchableOpacity onPress={onSelect} activeOpacity={0.7} style={styles.touchable}>
|
|
23
|
+
<View
|
|
24
|
+
style={[
|
|
25
|
+
styles.container,
|
|
26
|
+
{
|
|
27
|
+
backgroundColor: tokens.colors.surface,
|
|
28
|
+
borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
|
|
29
|
+
borderWidth: isSelected ? 2 : 1,
|
|
30
|
+
},
|
|
31
|
+
]}
|
|
32
|
+
>
|
|
33
|
+
{pkg.badge && (
|
|
34
|
+
<View style={styles.badgeContainer}>
|
|
35
|
+
<AtomicBadge text={pkg.badge} variant="warning" size="sm" />
|
|
36
|
+
</View>
|
|
37
|
+
)}
|
|
38
|
+
|
|
39
|
+
<View style={styles.content}>
|
|
40
|
+
<View style={styles.leftSection}>
|
|
41
|
+
<AtomicIcon name="flash" size="md" color={isSelected ? "primary" : "secondary"} />
|
|
42
|
+
<AtomicText
|
|
43
|
+
type="headlineSmall"
|
|
44
|
+
style={[styles.credits, { color: isSelected ? tokens.colors.primary : tokens.colors.textPrimary }]}
|
|
45
|
+
>
|
|
46
|
+
{totalCredits.toLocaleString()}
|
|
47
|
+
</AtomicText>
|
|
48
|
+
</View>
|
|
49
|
+
|
|
50
|
+
<View style={styles.rightSection}>
|
|
51
|
+
<AtomicText
|
|
52
|
+
type="titleLarge"
|
|
53
|
+
style={[styles.price, { color: isSelected ? tokens.colors.primary : tokens.colors.textPrimary }]}
|
|
54
|
+
>
|
|
55
|
+
{pkg.currency}{pkg.price.toFixed(2)}
|
|
56
|
+
</AtomicText>
|
|
57
|
+
{isSelected && <AtomicIcon name="checkmark-circle" size="md" color="primary" />}
|
|
58
|
+
</View>
|
|
59
|
+
</View>
|
|
60
|
+
|
|
61
|
+
{(pkg.bonus ?? 0) > 0 && (
|
|
62
|
+
<View style={styles.bonusRow}>
|
|
63
|
+
<AtomicIcon name="gift-outline" size="sm" color="success" />
|
|
64
|
+
<AtomicText type="bodySmall" style={{ color: tokens.colors.success, marginLeft: 4 }}>
|
|
65
|
+
+{pkg.bonus}
|
|
66
|
+
</AtomicText>
|
|
67
|
+
</View>
|
|
68
|
+
)}
|
|
69
|
+
</View>
|
|
70
|
+
</TouchableOpacity>
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
CreditCard.displayName = "CreditCard";
|
|
75
|
+
|
|
76
|
+
const styles = StyleSheet.create({
|
|
77
|
+
touchable: {
|
|
78
|
+
marginBottom: 10,
|
|
79
|
+
marginHorizontal: 24,
|
|
80
|
+
},
|
|
81
|
+
container: {
|
|
82
|
+
borderRadius: 16,
|
|
83
|
+
padding: 16,
|
|
84
|
+
position: "relative",
|
|
85
|
+
},
|
|
86
|
+
badgeContainer: {
|
|
87
|
+
position: "absolute",
|
|
88
|
+
top: -10,
|
|
89
|
+
right: 16,
|
|
90
|
+
},
|
|
91
|
+
content: {
|
|
92
|
+
flexDirection: "row",
|
|
93
|
+
justifyContent: "space-between",
|
|
94
|
+
alignItems: "center",
|
|
95
|
+
},
|
|
96
|
+
leftSection: {
|
|
97
|
+
flexDirection: "row",
|
|
98
|
+
alignItems: "center",
|
|
99
|
+
},
|
|
100
|
+
credits: {
|
|
101
|
+
fontWeight: "700",
|
|
102
|
+
marginLeft: 8,
|
|
103
|
+
},
|
|
104
|
+
rightSection: {
|
|
105
|
+
flexDirection: "row",
|
|
106
|
+
alignItems: "center",
|
|
107
|
+
},
|
|
108
|
+
price: {
|
|
109
|
+
fontWeight: "700",
|
|
110
|
+
marginRight: 8,
|
|
111
|
+
},
|
|
112
|
+
bonusRow: {
|
|
113
|
+
flexDirection: "row",
|
|
114
|
+
alignItems: "center",
|
|
115
|
+
marginTop: 8,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Item
|
|
3
|
+
* Single feature row with icon
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicText, AtomicIcon, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
|
|
10
|
+
interface FeatureItemProps {
|
|
11
|
+
icon: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const FeatureItem: React.FC<FeatureItemProps> = React.memo(({ icon, text }) => {
|
|
16
|
+
const tokens = useAppDesignTokens();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View style={styles.container}>
|
|
20
|
+
<View style={[styles.iconContainer, { backgroundColor: tokens.colors.primaryLight }]}>
|
|
21
|
+
<AtomicIcon
|
|
22
|
+
name={icon || "checkmark-circle"}
|
|
23
|
+
customSize={16}
|
|
24
|
+
customColor={tokens.colors.primary}
|
|
25
|
+
/>
|
|
26
|
+
</View>
|
|
27
|
+
<AtomicText type="bodyMedium" style={{ color: tokens.colors.textPrimary, flex: 1 }}>
|
|
28
|
+
{text}
|
|
29
|
+
</AtomicText>
|
|
30
|
+
</View>
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
FeatureItem.displayName = "FeatureItem";
|
|
35
|
+
|
|
36
|
+
const styles = StyleSheet.create({
|
|
37
|
+
container: {
|
|
38
|
+
flexDirection: "row",
|
|
39
|
+
alignItems: "center",
|
|
40
|
+
marginBottom: 12,
|
|
41
|
+
},
|
|
42
|
+
iconContainer: {
|
|
43
|
+
width: 28,
|
|
44
|
+
height: 28,
|
|
45
|
+
borderRadius: 14,
|
|
46
|
+
justifyContent: "center",
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
marginRight: 12,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature List
|
|
3
|
+
* List of premium features
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { FeatureItem } from "./FeatureItem";
|
|
9
|
+
import type { SubscriptionFeature } from "../entities";
|
|
10
|
+
|
|
11
|
+
interface FeatureListProps {
|
|
12
|
+
features: SubscriptionFeature[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const FeatureList: React.FC<FeatureListProps> = React.memo(({ features }) => {
|
|
16
|
+
if (features.length === 0) return null;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View style={styles.container}>
|
|
20
|
+
{features.map((feature, index) => (
|
|
21
|
+
<FeatureItem key={`${feature.icon}-${index}`} icon={feature.icon} text={feature.text} />
|
|
22
|
+
))}
|
|
23
|
+
</View>
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
FeatureList.displayName = "FeatureList";
|
|
28
|
+
|
|
29
|
+
const styles = StyleSheet.create({
|
|
30
|
+
container: {
|
|
31
|
+
paddingHorizontal: 24,
|
|
32
|
+
marginBottom: 20,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Footer
|
|
3
|
+
* Action button and legal links
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet, Linking } from "react-native";
|
|
8
|
+
import { AtomicText, AtomicButton, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
|
|
10
|
+
interface PaywallFooterProps {
|
|
11
|
+
isProcessing: boolean;
|
|
12
|
+
isDisabled: boolean;
|
|
13
|
+
purchaseButtonText: string;
|
|
14
|
+
processingText: string;
|
|
15
|
+
restoreButtonText: string;
|
|
16
|
+
privacyText?: string;
|
|
17
|
+
termsText?: string;
|
|
18
|
+
privacyUrl?: string;
|
|
19
|
+
termsUrl?: string;
|
|
20
|
+
onPurchase: () => void;
|
|
21
|
+
onRestore: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const PaywallFooter: React.FC<PaywallFooterProps> = React.memo(
|
|
25
|
+
({
|
|
26
|
+
isProcessing,
|
|
27
|
+
isDisabled,
|
|
28
|
+
purchaseButtonText,
|
|
29
|
+
processingText,
|
|
30
|
+
restoreButtonText,
|
|
31
|
+
privacyText,
|
|
32
|
+
termsText,
|
|
33
|
+
privacyUrl,
|
|
34
|
+
termsUrl,
|
|
35
|
+
onPurchase,
|
|
36
|
+
onRestore,
|
|
37
|
+
}) => {
|
|
38
|
+
const tokens = useAppDesignTokens();
|
|
39
|
+
|
|
40
|
+
const handleOpenUrl = (url?: string) => {
|
|
41
|
+
if (url) Linking.openURL(url);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<View style={styles.container}>
|
|
46
|
+
<AtomicButton
|
|
47
|
+
title={isProcessing ? processingText : purchaseButtonText}
|
|
48
|
+
onPress={onPurchase}
|
|
49
|
+
disabled={isDisabled || isProcessing}
|
|
50
|
+
variant="primary"
|
|
51
|
+
size="lg"
|
|
52
|
+
style={styles.purchaseButton}
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
<View style={styles.linksRow}>
|
|
56
|
+
{termsText && termsUrl && (
|
|
57
|
+
<TouchableOpacity onPress={() => handleOpenUrl(termsUrl)}>
|
|
58
|
+
<AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
|
|
59
|
+
{termsText}
|
|
60
|
+
</AtomicText>
|
|
61
|
+
</TouchableOpacity>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
<TouchableOpacity onPress={onRestore}>
|
|
65
|
+
<AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
|
|
66
|
+
{restoreButtonText}
|
|
67
|
+
</AtomicText>
|
|
68
|
+
</TouchableOpacity>
|
|
69
|
+
|
|
70
|
+
{privacyText && privacyUrl && (
|
|
71
|
+
<TouchableOpacity onPress={() => handleOpenUrl(privacyUrl)}>
|
|
72
|
+
<AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
|
|
73
|
+
{privacyText}
|
|
74
|
+
</AtomicText>
|
|
75
|
+
</TouchableOpacity>
|
|
76
|
+
)}
|
|
77
|
+
</View>
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
PaywallFooter.displayName = "PaywallFooter";
|
|
84
|
+
|
|
85
|
+
const styles = StyleSheet.create({
|
|
86
|
+
container: {
|
|
87
|
+
paddingHorizontal: 24,
|
|
88
|
+
paddingBottom: 32,
|
|
89
|
+
},
|
|
90
|
+
purchaseButton: {
|
|
91
|
+
marginBottom: 16,
|
|
92
|
+
},
|
|
93
|
+
linksRow: {
|
|
94
|
+
flexDirection: "row",
|
|
95
|
+
justifyContent: "space-between",
|
|
96
|
+
paddingHorizontal: 8,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Paywall
|
|
3
|
-
* Header with gradient
|
|
2
|
+
* Paywall Header
|
|
3
|
+
* Header with gradient, close button, title and subtitle
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React from "react";
|
|
@@ -13,21 +13,21 @@ import {
|
|
|
13
13
|
useAppDesignTokens,
|
|
14
14
|
} from "@umituz/react-native-design-system";
|
|
15
15
|
|
|
16
|
-
interface
|
|
16
|
+
interface PaywallHeaderProps {
|
|
17
17
|
title: string;
|
|
18
18
|
subtitle?: string;
|
|
19
19
|
onClose: () => void;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export const
|
|
22
|
+
export const PaywallHeader: React.FC<PaywallHeaderProps> = React.memo(
|
|
23
23
|
({ title, subtitle, onClose }) => {
|
|
24
24
|
const tokens = useAppDesignTokens();
|
|
25
25
|
const { themeMode } = useDesignSystemTheme();
|
|
26
26
|
const isDark = themeMode === "dark";
|
|
27
27
|
|
|
28
|
-
const gradientColors: readonly [string, string
|
|
29
|
-
? [tokens.colors.
|
|
30
|
-
: [tokens.colors.primary, tokens.colors.primaryDark
|
|
28
|
+
const gradientColors: readonly [string, string] = isDark
|
|
29
|
+
? [tokens.colors.surface, tokens.colors.surfaceSecondary]
|
|
30
|
+
: [tokens.colors.primary, tokens.colors.primaryDark];
|
|
31
31
|
|
|
32
32
|
return (
|
|
33
33
|
<LinearGradient
|
|
@@ -36,19 +36,9 @@ export const PaywallHeroHeader: React.FC<PaywallHeroHeaderProps> = React.memo(
|
|
|
36
36
|
end={{ x: 1, y: 1 }}
|
|
37
37
|
style={styles.container}
|
|
38
38
|
>
|
|
39
|
-
<View style={[styles.decorativeCircle, styles.circle1]} />
|
|
40
|
-
<View style={[styles.decorativeCircle, styles.circle2]} />
|
|
41
|
-
|
|
42
39
|
<TouchableOpacity
|
|
43
40
|
onPress={onClose}
|
|
44
|
-
style={[
|
|
45
|
-
styles.closeButton,
|
|
46
|
-
{
|
|
47
|
-
backgroundColor: isDark
|
|
48
|
-
? tokens.colors.surfaceSecondary
|
|
49
|
-
: tokens.colors.onPrimary,
|
|
50
|
-
},
|
|
51
|
-
]}
|
|
41
|
+
style={[styles.closeButton, { backgroundColor: tokens.colors.onPrimary }]}
|
|
52
42
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
53
43
|
>
|
|
54
44
|
<AtomicIcon
|
|
@@ -81,36 +71,18 @@ export const PaywallHeroHeader: React.FC<PaywallHeroHeaderProps> = React.memo(
|
|
|
81
71
|
}
|
|
82
72
|
);
|
|
83
73
|
|
|
84
|
-
|
|
74
|
+
PaywallHeader.displayName = "PaywallHeader";
|
|
85
75
|
|
|
86
76
|
const styles = StyleSheet.create({
|
|
87
77
|
container: {
|
|
88
|
-
paddingTop:
|
|
89
|
-
paddingBottom:
|
|
78
|
+
paddingTop: 56,
|
|
79
|
+
paddingBottom: 36,
|
|
90
80
|
paddingHorizontal: 24,
|
|
91
81
|
position: "relative",
|
|
92
|
-
overflow: "hidden",
|
|
93
|
-
},
|
|
94
|
-
decorativeCircle: {
|
|
95
|
-
position: "absolute",
|
|
96
|
-
borderRadius: 9999,
|
|
97
|
-
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
|
98
|
-
},
|
|
99
|
-
circle1: {
|
|
100
|
-
width: 200,
|
|
101
|
-
height: 200,
|
|
102
|
-
top: -100,
|
|
103
|
-
right: -50,
|
|
104
|
-
},
|
|
105
|
-
circle2: {
|
|
106
|
-
width: 150,
|
|
107
|
-
height: 150,
|
|
108
|
-
bottom: -75,
|
|
109
|
-
left: -40,
|
|
110
82
|
},
|
|
111
83
|
closeButton: {
|
|
112
84
|
position: "absolute",
|
|
113
|
-
top:
|
|
85
|
+
top: 48,
|
|
114
86
|
right: 20,
|
|
115
87
|
width: 36,
|
|
116
88
|
height: 36,
|
|
@@ -121,7 +93,6 @@ const styles = StyleSheet.create({
|
|
|
121
93
|
},
|
|
122
94
|
content: {
|
|
123
95
|
alignItems: "center",
|
|
124
|
-
zIndex: 1,
|
|
125
96
|
},
|
|
126
97
|
title: {
|
|
127
98
|
fontWeight: "700",
|
|
@@ -137,8 +108,8 @@ const styles = StyleSheet.create({
|
|
|
137
108
|
bottom: -1,
|
|
138
109
|
left: 0,
|
|
139
110
|
right: 0,
|
|
140
|
-
height:
|
|
141
|
-
borderTopLeftRadius:
|
|
142
|
-
borderTopRightRadius:
|
|
111
|
+
height: 24,
|
|
112
|
+
borderTopLeftRadius: 24,
|
|
113
|
+
borderTopRightRadius: 24,
|
|
143
114
|
},
|
|
144
115
|
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Modal
|
|
3
|
+
* Mode-based paywall: subscription, credits, or hybrid
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useState, useCallback } from "react";
|
|
7
|
+
import { View, ScrollView, StyleSheet, ActivityIndicator } from "react-native";
|
|
8
|
+
import { BaseModal, useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
9
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
10
|
+
import { PaywallHeader } from "./PaywallHeader";
|
|
11
|
+
import { PaywallTabBar } from "./PaywallTabBar";
|
|
12
|
+
import { PaywallFooter } from "./PaywallFooter";
|
|
13
|
+
import { FeatureList } from "./FeatureList";
|
|
14
|
+
import { PlanCard } from "./PlanCard";
|
|
15
|
+
import { CreditCard } from "./CreditCard";
|
|
16
|
+
import type {
|
|
17
|
+
PaywallMode,
|
|
18
|
+
PaywallTabType,
|
|
19
|
+
CreditsPackage,
|
|
20
|
+
SubscriptionFeature,
|
|
21
|
+
PaywallTranslations,
|
|
22
|
+
PaywallLegalUrls,
|
|
23
|
+
} from "../entities";
|
|
24
|
+
|
|
25
|
+
export interface PaywallModalProps {
|
|
26
|
+
visible: boolean;
|
|
27
|
+
onClose: () => void;
|
|
28
|
+
mode: PaywallMode;
|
|
29
|
+
translations: PaywallTranslations;
|
|
30
|
+
subscriptionPackages?: PurchasesPackage[];
|
|
31
|
+
creditsPackages?: CreditsPackage[];
|
|
32
|
+
features?: SubscriptionFeature[];
|
|
33
|
+
isLoading?: boolean;
|
|
34
|
+
legalUrls?: PaywallLegalUrls;
|
|
35
|
+
bestValueIdentifier?: string;
|
|
36
|
+
creditAmounts?: Record<string, number>;
|
|
37
|
+
creditsLabel?: string;
|
|
38
|
+
onSubscriptionPurchase?: (pkg: PurchasesPackage) => Promise<void>;
|
|
39
|
+
onCreditsPurchase?: (packageId: string) => Promise<void>;
|
|
40
|
+
onRestore?: () => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
44
|
+
const {
|
|
45
|
+
visible,
|
|
46
|
+
onClose,
|
|
47
|
+
mode,
|
|
48
|
+
translations,
|
|
49
|
+
subscriptionPackages = [],
|
|
50
|
+
creditsPackages = [],
|
|
51
|
+
features = [],
|
|
52
|
+
isLoading = false,
|
|
53
|
+
legalUrls = {},
|
|
54
|
+
bestValueIdentifier,
|
|
55
|
+
creditAmounts,
|
|
56
|
+
creditsLabel,
|
|
57
|
+
onSubscriptionPurchase,
|
|
58
|
+
onCreditsPurchase,
|
|
59
|
+
onRestore,
|
|
60
|
+
} = props;
|
|
61
|
+
|
|
62
|
+
const tokens = useAppDesignTokens();
|
|
63
|
+
const initialTab: PaywallTabType = mode === "credits" ? "credits" : "subscription";
|
|
64
|
+
const [activeTab, setActiveTab] = useState<PaywallTabType>(initialTab);
|
|
65
|
+
const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
|
|
66
|
+
const [selectedCreditId, setSelectedCreditId] = useState<string | null>(null);
|
|
67
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
68
|
+
|
|
69
|
+
const showTabs = mode === "hybrid";
|
|
70
|
+
const showCredits = mode === "credits" || (mode === "hybrid" && activeTab === "credits");
|
|
71
|
+
const showSubscription = mode === "subscription" || (mode === "hybrid" && activeTab === "subscription");
|
|
72
|
+
|
|
73
|
+
const handlePurchase = useCallback(async () => {
|
|
74
|
+
setIsProcessing(true);
|
|
75
|
+
try {
|
|
76
|
+
if (showSubscription && selectedPlanId && onSubscriptionPurchase) {
|
|
77
|
+
const pkg = subscriptionPackages.find((p) => p.product.identifier === selectedPlanId);
|
|
78
|
+
if (pkg) await onSubscriptionPurchase(pkg);
|
|
79
|
+
} else if (showCredits && selectedCreditId && onCreditsPurchase) {
|
|
80
|
+
await onCreditsPurchase(selectedCreditId);
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
setIsProcessing(false);
|
|
84
|
+
}
|
|
85
|
+
}, [showSubscription, showCredits, selectedPlanId, selectedCreditId, subscriptionPackages, onSubscriptionPurchase, onCreditsPurchase]);
|
|
86
|
+
|
|
87
|
+
const handleRestore = useCallback(async () => {
|
|
88
|
+
if (onRestore) {
|
|
89
|
+
setIsProcessing(true);
|
|
90
|
+
try {
|
|
91
|
+
await onRestore();
|
|
92
|
+
} finally {
|
|
93
|
+
setIsProcessing(false);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}, [onRestore]);
|
|
97
|
+
|
|
98
|
+
const isPurchaseDisabled = showSubscription ? !selectedPlanId : !selectedCreditId;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<BaseModal visible={visible} onClose={onClose}>
|
|
102
|
+
<View style={styles.container}>
|
|
103
|
+
<PaywallHeader title={translations.title} subtitle={translations.subtitle} onClose={onClose} />
|
|
104
|
+
|
|
105
|
+
{showTabs && (
|
|
106
|
+
<PaywallTabBar
|
|
107
|
+
activeTab={activeTab}
|
|
108
|
+
onTabChange={setActiveTab}
|
|
109
|
+
creditsLabel={translations.creditsTabLabel ?? "Credits"}
|
|
110
|
+
subscriptionLabel={translations.subscriptionTabLabel ?? "Subscription"}
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
<ScrollView style={styles.scroll} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
|
115
|
+
<FeatureList features={features} />
|
|
116
|
+
|
|
117
|
+
{isLoading ? (
|
|
118
|
+
<View style={styles.loadingContainer}>
|
|
119
|
+
<ActivityIndicator color={tokens.colors.primary} />
|
|
120
|
+
<AtomicText type="bodyMedium" style={{ color: tokens.colors.textSecondary, marginTop: 12 }}>
|
|
121
|
+
{translations.loadingText}
|
|
122
|
+
</AtomicText>
|
|
123
|
+
</View>
|
|
124
|
+
) : (
|
|
125
|
+
<>
|
|
126
|
+
{showSubscription &&
|
|
127
|
+
subscriptionPackages.map((pkg) => (
|
|
128
|
+
<PlanCard
|
|
129
|
+
key={pkg.product.identifier}
|
|
130
|
+
pkg={pkg}
|
|
131
|
+
isSelected={selectedPlanId === pkg.product.identifier}
|
|
132
|
+
onSelect={() => setSelectedPlanId(pkg.product.identifier)}
|
|
133
|
+
badge={pkg.product.identifier === bestValueIdentifier ? "Best Value" : undefined}
|
|
134
|
+
creditAmount={creditAmounts?.[pkg.product.identifier]}
|
|
135
|
+
creditsLabel={creditsLabel}
|
|
136
|
+
/>
|
|
137
|
+
))}
|
|
138
|
+
|
|
139
|
+
{showCredits &&
|
|
140
|
+
creditsPackages.map((pkg) => (
|
|
141
|
+
<CreditCard
|
|
142
|
+
key={pkg.id}
|
|
143
|
+
pkg={pkg}
|
|
144
|
+
isSelected={selectedCreditId === pkg.id}
|
|
145
|
+
onSelect={() => setSelectedCreditId(pkg.id)}
|
|
146
|
+
/>
|
|
147
|
+
))}
|
|
148
|
+
</>
|
|
149
|
+
)}
|
|
150
|
+
</ScrollView>
|
|
151
|
+
|
|
152
|
+
<PaywallFooter
|
|
153
|
+
isProcessing={isProcessing}
|
|
154
|
+
isDisabled={isPurchaseDisabled}
|
|
155
|
+
purchaseButtonText={showSubscription ? (translations.subscribeButtonText ?? translations.purchaseButtonText) : translations.purchaseButtonText}
|
|
156
|
+
processingText={translations.processingText}
|
|
157
|
+
restoreButtonText={translations.restoreButtonText}
|
|
158
|
+
privacyText={translations.privacyText}
|
|
159
|
+
termsText={translations.termsOfServiceText}
|
|
160
|
+
privacyUrl={legalUrls.privacyUrl}
|
|
161
|
+
termsUrl={legalUrls.termsUrl}
|
|
162
|
+
onPurchase={handlePurchase}
|
|
163
|
+
onRestore={handleRestore}
|
|
164
|
+
/>
|
|
165
|
+
</View>
|
|
166
|
+
</BaseModal>
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
PaywallModal.displayName = "PaywallModal";
|
|
171
|
+
|
|
172
|
+
const styles = StyleSheet.create({
|
|
173
|
+
container: {
|
|
174
|
+
flex: 1,
|
|
175
|
+
width: "100%",
|
|
176
|
+
},
|
|
177
|
+
scroll: {
|
|
178
|
+
flex: 1,
|
|
179
|
+
},
|
|
180
|
+
scrollContent: {
|
|
181
|
+
paddingBottom: 20,
|
|
182
|
+
},
|
|
183
|
+
loadingContainer: {
|
|
184
|
+
alignItems: "center",
|
|
185
|
+
paddingVertical: 40,
|
|
186
|
+
},
|
|
187
|
+
});
|