@umituz/react-native-subscription 1.4.0 → 1.5.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 +11 -10
- package/src/domain/entities/paywall/CreditsPackage.ts +16 -0
- package/src/domain/entities/paywall/PaywallTab.ts +11 -0
- package/src/domain/entities/paywall/SubscriptionPlan.ts +27 -0
- package/src/index.ts +47 -38
- package/src/presentation/components/paywall/CreditsPackageCard.tsx +129 -0
- package/src/presentation/components/paywall/CreditsTabContent.tsx +130 -0
- package/src/presentation/components/paywall/PaywallFeatureItem.tsx +66 -0
- package/src/presentation/components/paywall/PaywallFeaturesList.tsx +43 -0
- package/src/presentation/components/paywall/PaywallHeader.tsx +82 -0
- package/src/presentation/components/paywall/PaywallLegalFooter.tsx +54 -0
- package/src/presentation/components/paywall/PaywallModal.tsx +161 -0
- package/src/presentation/components/paywall/PaywallTabBar.tsx +96 -0
- package/src/presentation/components/paywall/SubscriptionModal.tsx +206 -0
- package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +186 -0
- package/src/presentation/components/paywall/SubscriptionTabContent.tsx +142 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Header Component
|
|
3
|
+
* Single Responsibility: Display paywall header with close button
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system-atoms";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
|
+
|
|
11
|
+
interface PaywallHeaderProps {
|
|
12
|
+
title: string;
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const PaywallHeader: React.FC<PaywallHeaderProps> = React.memo(
|
|
18
|
+
({ title, subtitle, onClose }) => {
|
|
19
|
+
const tokens = useAppDesignTokens();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<View style={styles.container}>
|
|
23
|
+
<View style={styles.titleContainer}>
|
|
24
|
+
<AtomicText
|
|
25
|
+
type="headlineLarge"
|
|
26
|
+
style={[styles.title, { color: tokens.colors.textPrimary }]}
|
|
27
|
+
>
|
|
28
|
+
{title}
|
|
29
|
+
</AtomicText>
|
|
30
|
+
{subtitle && (
|
|
31
|
+
<AtomicText
|
|
32
|
+
type="bodyMedium"
|
|
33
|
+
style={[styles.subtitle, { color: tokens.colors.textSecondary }]}
|
|
34
|
+
>
|
|
35
|
+
{subtitle}
|
|
36
|
+
</AtomicText>
|
|
37
|
+
)}
|
|
38
|
+
</View>
|
|
39
|
+
<TouchableOpacity
|
|
40
|
+
onPress={onClose}
|
|
41
|
+
style={[
|
|
42
|
+
styles.closeButton,
|
|
43
|
+
{ backgroundColor: tokens.colors.surfaceSecondary },
|
|
44
|
+
]}
|
|
45
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
46
|
+
>
|
|
47
|
+
<AtomicIcon name="X" size="md" color="secondary" />
|
|
48
|
+
</TouchableOpacity>
|
|
49
|
+
</View>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
PaywallHeader.displayName = "PaywallHeader";
|
|
55
|
+
|
|
56
|
+
const styles = StyleSheet.create({
|
|
57
|
+
container: {
|
|
58
|
+
flexDirection: "row",
|
|
59
|
+
justifyContent: "space-between",
|
|
60
|
+
alignItems: "flex-start",
|
|
61
|
+
paddingHorizontal: 24,
|
|
62
|
+
paddingTop: 8,
|
|
63
|
+
paddingBottom: 16,
|
|
64
|
+
},
|
|
65
|
+
titleContainer: {
|
|
66
|
+
flex: 1,
|
|
67
|
+
marginRight: 16,
|
|
68
|
+
},
|
|
69
|
+
title: {
|
|
70
|
+
fontWeight: "700",
|
|
71
|
+
},
|
|
72
|
+
subtitle: {
|
|
73
|
+
marginTop: 4,
|
|
74
|
+
},
|
|
75
|
+
closeButton: {
|
|
76
|
+
width: 32,
|
|
77
|
+
height: 32,
|
|
78
|
+
borderRadius: 16,
|
|
79
|
+
justifyContent: "center",
|
|
80
|
+
alignItems: "center",
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Legal Footer Component
|
|
3
|
+
* Display legal links and terms for App Store compliance
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicText } from "@umituz/react-native-design-system-atoms";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
|
+
import { LegalLinks } from "@umituz/react-native-legal";
|
|
11
|
+
|
|
12
|
+
interface PaywallLegalFooterProps {
|
|
13
|
+
termsText?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_TERMS =
|
|
17
|
+
"Payment will be charged to your account. Subscription automatically renews unless cancelled.";
|
|
18
|
+
|
|
19
|
+
export const PaywallLegalFooter: React.FC<PaywallLegalFooterProps> = React.memo(
|
|
20
|
+
({ termsText = DEFAULT_TERMS }) => {
|
|
21
|
+
const tokens = useAppDesignTokens();
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<View style={styles.container}>
|
|
25
|
+
<AtomicText
|
|
26
|
+
type="labelSmall"
|
|
27
|
+
style={[styles.termsText, { color: tokens.colors.textTertiary }]}
|
|
28
|
+
>
|
|
29
|
+
{termsText}
|
|
30
|
+
</AtomicText>
|
|
31
|
+
<LegalLinks style={styles.legalLinks} />
|
|
32
|
+
</View>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
PaywallLegalFooter.displayName = "PaywallLegalFooter";
|
|
38
|
+
|
|
39
|
+
const styles = StyleSheet.create({
|
|
40
|
+
container: {
|
|
41
|
+
alignItems: "center",
|
|
42
|
+
paddingHorizontal: 24,
|
|
43
|
+
paddingBottom: 16,
|
|
44
|
+
},
|
|
45
|
+
termsText: {
|
|
46
|
+
textAlign: "center",
|
|
47
|
+
fontSize: 11,
|
|
48
|
+
lineHeight: 16,
|
|
49
|
+
marginBottom: 8,
|
|
50
|
+
},
|
|
51
|
+
legalLinks: {
|
|
52
|
+
marginTop: 4,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Modal Component
|
|
3
|
+
* Single Responsibility: Display paywall with Credits and Subscription tabs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useEffect } from "react";
|
|
7
|
+
import { View, Modal, StyleSheet, TouchableOpacity } from "react-native";
|
|
8
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
11
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
12
|
+
import { usePaywall } from "../hooks/usePaywall";
|
|
13
|
+
import { PaywallHeader } from "./PaywallHeader";
|
|
14
|
+
import { PaywallTabBar } from "./PaywallTabBar";
|
|
15
|
+
import { CreditsTabContent } from "./CreditsTabContent";
|
|
16
|
+
import { SubscriptionTabContent } from "./SubscriptionTabContent";
|
|
17
|
+
import type { PaywallTabType } from "../../domain/entities/PaywallTab";
|
|
18
|
+
import type { CreditsPackage } from "../../domain/entities/CreditsPackage";
|
|
19
|
+
|
|
20
|
+
interface PaywallModalProps {
|
|
21
|
+
visible: boolean;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
initialTab?: PaywallTabType;
|
|
24
|
+
creditsPackages: CreditsPackage[];
|
|
25
|
+
subscriptionPackages: PurchasesPackage[];
|
|
26
|
+
currentCredits: number;
|
|
27
|
+
requiredCredits?: number;
|
|
28
|
+
onCreditsPurchase: (packageId: string) => Promise<void>;
|
|
29
|
+
onSubscriptionPurchase: (pkg: PurchasesPackage) => Promise<void>;
|
|
30
|
+
subscriptionFeatures?: Array<{ icon: string; text: string }>;
|
|
31
|
+
isLoading?: boolean;
|
|
32
|
+
title?: string;
|
|
33
|
+
subtitle?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const PaywallModal: React.FC<PaywallModalProps> = React.memo(
|
|
37
|
+
({
|
|
38
|
+
visible,
|
|
39
|
+
onClose,
|
|
40
|
+
initialTab = "credits",
|
|
41
|
+
creditsPackages,
|
|
42
|
+
subscriptionPackages,
|
|
43
|
+
currentCredits,
|
|
44
|
+
requiredCredits,
|
|
45
|
+
onCreditsPurchase,
|
|
46
|
+
onSubscriptionPurchase,
|
|
47
|
+
subscriptionFeatures = [],
|
|
48
|
+
isLoading = false,
|
|
49
|
+
title,
|
|
50
|
+
subtitle,
|
|
51
|
+
}) => {
|
|
52
|
+
const tokens = useAppDesignTokens();
|
|
53
|
+
const { t } = useLocalization();
|
|
54
|
+
|
|
55
|
+
const {
|
|
56
|
+
activeTab,
|
|
57
|
+
selectedCreditsPackageId,
|
|
58
|
+
selectedSubscriptionPkg,
|
|
59
|
+
handleTabChange,
|
|
60
|
+
handleCreditsPackageSelect,
|
|
61
|
+
handleSubscriptionPackageSelect,
|
|
62
|
+
handleCreditsPurchase,
|
|
63
|
+
handleSubscriptionPurchase,
|
|
64
|
+
} = usePaywall({
|
|
65
|
+
initialTab,
|
|
66
|
+
onCreditsPurchase,
|
|
67
|
+
onSubscriptionPurchase,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const displayTitle = title || t("paywall.title", { defaultValue: "Get Premium" });
|
|
71
|
+
const displaySubtitle = subtitle || t("paywall.subtitle", { defaultValue: "" });
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (__DEV__) {
|
|
75
|
+
console.log("[PaywallModal] Visibility changed:", visible);
|
|
76
|
+
}
|
|
77
|
+
}, [visible]);
|
|
78
|
+
|
|
79
|
+
if (!visible) return null;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Modal
|
|
83
|
+
visible={visible}
|
|
84
|
+
transparent
|
|
85
|
+
animationType="slide"
|
|
86
|
+
onRequestClose={onClose}
|
|
87
|
+
>
|
|
88
|
+
<View style={styles.overlay}>
|
|
89
|
+
<TouchableOpacity
|
|
90
|
+
style={styles.backdrop}
|
|
91
|
+
activeOpacity={1}
|
|
92
|
+
onPress={onClose}
|
|
93
|
+
/>
|
|
94
|
+
<View
|
|
95
|
+
style={[styles.content, { backgroundColor: tokens.colors.surface }]}
|
|
96
|
+
>
|
|
97
|
+
<SafeAreaView edges={["top"]} style={styles.safeArea}>
|
|
98
|
+
<PaywallHeader
|
|
99
|
+
title={displayTitle}
|
|
100
|
+
subtitle={displaySubtitle}
|
|
101
|
+
onClose={onClose}
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
<PaywallTabBar
|
|
105
|
+
activeTab={activeTab}
|
|
106
|
+
onTabChange={handleTabChange}
|
|
107
|
+
creditsLabel={t("paywall.tabs.credits", { defaultValue: "Credits" })}
|
|
108
|
+
subscriptionLabel={t("paywall.tabs.subscription", { defaultValue: "Subscription" })}
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
{activeTab === "credits" ? (
|
|
112
|
+
<CreditsTabContent
|
|
113
|
+
packages={creditsPackages}
|
|
114
|
+
selectedPackageId={selectedCreditsPackageId}
|
|
115
|
+
onSelectPackage={handleCreditsPackageSelect}
|
|
116
|
+
onPurchase={handleCreditsPurchase}
|
|
117
|
+
currentCredits={currentCredits}
|
|
118
|
+
requiredCredits={requiredCredits}
|
|
119
|
+
isLoading={isLoading}
|
|
120
|
+
purchaseButtonText={t("paywall.purchase", { defaultValue: "Purchase" })}
|
|
121
|
+
/>
|
|
122
|
+
) : (
|
|
123
|
+
<SubscriptionTabContent
|
|
124
|
+
packages={subscriptionPackages}
|
|
125
|
+
selectedPackage={selectedSubscriptionPkg}
|
|
126
|
+
onSelectPackage={handleSubscriptionPackageSelect}
|
|
127
|
+
onPurchase={handleSubscriptionPurchase}
|
|
128
|
+
features={subscriptionFeatures}
|
|
129
|
+
isLoading={isLoading}
|
|
130
|
+
purchaseButtonText={t("paywall.subscribe", { defaultValue: "Subscribe" })}
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
</SafeAreaView>
|
|
134
|
+
</View>
|
|
135
|
+
</View>
|
|
136
|
+
</Modal>
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
PaywallModal.displayName = "PaywallModal";
|
|
142
|
+
|
|
143
|
+
const styles = StyleSheet.create({
|
|
144
|
+
overlay: {
|
|
145
|
+
flex: 1,
|
|
146
|
+
justifyContent: "flex-end",
|
|
147
|
+
},
|
|
148
|
+
backdrop: {
|
|
149
|
+
...StyleSheet.absoluteFillObject,
|
|
150
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
151
|
+
},
|
|
152
|
+
content: {
|
|
153
|
+
borderTopLeftRadius: 24,
|
|
154
|
+
borderTopRightRadius: 24,
|
|
155
|
+
maxHeight: "90%",
|
|
156
|
+
flex: 1,
|
|
157
|
+
},
|
|
158
|
+
safeArea: {
|
|
159
|
+
flex: 1,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Tab Bar Component
|
|
3
|
+
* Single Responsibility: Display and handle tab selection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicText } from "@umituz/react-native-design-system-atoms";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
|
+
import type { PaywallTabType } from "../../domain/entities/PaywallTab";
|
|
11
|
+
|
|
12
|
+
interface PaywallTabBarProps {
|
|
13
|
+
activeTab: PaywallTabType;
|
|
14
|
+
onTabChange: (tab: PaywallTabType) => void;
|
|
15
|
+
creditsLabel?: string;
|
|
16
|
+
subscriptionLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const PaywallTabBar: React.FC<PaywallTabBarProps> = React.memo(
|
|
20
|
+
({
|
|
21
|
+
activeTab,
|
|
22
|
+
onTabChange,
|
|
23
|
+
creditsLabel = "Credits",
|
|
24
|
+
subscriptionLabel = "Subscription",
|
|
25
|
+
}) => {
|
|
26
|
+
const tokens = useAppDesignTokens();
|
|
27
|
+
|
|
28
|
+
const renderTab = (tab: PaywallTabType, label: string) => {
|
|
29
|
+
const isActive = activeTab === tab;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<TouchableOpacity
|
|
33
|
+
key={tab}
|
|
34
|
+
style={[
|
|
35
|
+
styles.tab,
|
|
36
|
+
{
|
|
37
|
+
backgroundColor: isActive
|
|
38
|
+
? tokens.colors.primary
|
|
39
|
+
: tokens.colors.surfaceSecondary,
|
|
40
|
+
},
|
|
41
|
+
]}
|
|
42
|
+
onPress={() => onTabChange(tab)}
|
|
43
|
+
activeOpacity={0.8}
|
|
44
|
+
>
|
|
45
|
+
<AtomicText
|
|
46
|
+
type="labelLarge"
|
|
47
|
+
style={[
|
|
48
|
+
styles.tabText,
|
|
49
|
+
{
|
|
50
|
+
color: isActive
|
|
51
|
+
? tokens.colors.onPrimary
|
|
52
|
+
: tokens.colors.textSecondary,
|
|
53
|
+
},
|
|
54
|
+
]}
|
|
55
|
+
>
|
|
56
|
+
{label}
|
|
57
|
+
</AtomicText>
|
|
58
|
+
</TouchableOpacity>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<View
|
|
64
|
+
style={[
|
|
65
|
+
styles.container,
|
|
66
|
+
{ backgroundColor: tokens.colors.surfaceSecondary },
|
|
67
|
+
]}
|
|
68
|
+
>
|
|
69
|
+
{renderTab("credits", creditsLabel)}
|
|
70
|
+
{renderTab("subscription", subscriptionLabel)}
|
|
71
|
+
</View>
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
PaywallTabBar.displayName = "PaywallTabBar";
|
|
77
|
+
|
|
78
|
+
const styles = StyleSheet.create({
|
|
79
|
+
container: {
|
|
80
|
+
flexDirection: "row",
|
|
81
|
+
borderRadius: 12,
|
|
82
|
+
padding: 4,
|
|
83
|
+
marginHorizontal: 24,
|
|
84
|
+
marginBottom: 16,
|
|
85
|
+
},
|
|
86
|
+
tab: {
|
|
87
|
+
flex: 1,
|
|
88
|
+
paddingVertical: 12,
|
|
89
|
+
borderRadius: 8,
|
|
90
|
+
alignItems: "center",
|
|
91
|
+
justifyContent: "center",
|
|
92
|
+
},
|
|
93
|
+
tabText: {
|
|
94
|
+
fontWeight: "600",
|
|
95
|
+
},
|
|
96
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Modal Component
|
|
3
|
+
* Modal for displaying subscription packages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useState, useCallback } from "react";
|
|
7
|
+
import {
|
|
8
|
+
View,
|
|
9
|
+
Modal,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
TouchableOpacity,
|
|
12
|
+
ScrollView,
|
|
13
|
+
ActivityIndicator,
|
|
14
|
+
} from "react-native";
|
|
15
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
16
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
17
|
+
import { AtomicText, AtomicButton } from "@umituz/react-native-design-system-atoms";
|
|
18
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
19
|
+
import { SubscriptionPlanCard } from "./SubscriptionPlanCard";
|
|
20
|
+
import { PaywallFeaturesList } from "./PaywallFeaturesList";
|
|
21
|
+
import { PaywallLegalFooter } from "./PaywallLegalFooter";
|
|
22
|
+
|
|
23
|
+
export interface SubscriptionModalProps {
|
|
24
|
+
visible: boolean;
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
packages: PurchasesPackage[];
|
|
27
|
+
onPurchase: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
28
|
+
onRestore: () => Promise<boolean>;
|
|
29
|
+
title?: string;
|
|
30
|
+
subtitle?: string;
|
|
31
|
+
features?: Array<{ icon: string; text: string }>;
|
|
32
|
+
isLoading?: boolean;
|
|
33
|
+
purchaseButtonText?: string;
|
|
34
|
+
restoreButtonText?: string;
|
|
35
|
+
loadingText?: string;
|
|
36
|
+
emptyText?: string;
|
|
37
|
+
processingText?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo(
|
|
41
|
+
({
|
|
42
|
+
visible,
|
|
43
|
+
onClose,
|
|
44
|
+
packages,
|
|
45
|
+
onPurchase,
|
|
46
|
+
onRestore,
|
|
47
|
+
title = "Go Premium",
|
|
48
|
+
subtitle,
|
|
49
|
+
features = [],
|
|
50
|
+
isLoading = false,
|
|
51
|
+
purchaseButtonText = "Subscribe",
|
|
52
|
+
restoreButtonText = "Restore Purchases",
|
|
53
|
+
loadingText = "Loading packages...",
|
|
54
|
+
emptyText = "No packages available",
|
|
55
|
+
processingText = "Processing...",
|
|
56
|
+
}) => {
|
|
57
|
+
const tokens = useAppDesignTokens();
|
|
58
|
+
const [selectedPkg, setSelectedPkg] = useState<PurchasesPackage | null>(null);
|
|
59
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
60
|
+
|
|
61
|
+
const handlePurchase = useCallback(async () => {
|
|
62
|
+
if (!selectedPkg || isProcessing) return;
|
|
63
|
+
setIsProcessing(true);
|
|
64
|
+
try {
|
|
65
|
+
const success = await onPurchase(selectedPkg);
|
|
66
|
+
if (success) onClose();
|
|
67
|
+
} finally {
|
|
68
|
+
setIsProcessing(false);
|
|
69
|
+
}
|
|
70
|
+
}, [selectedPkg, isProcessing, onPurchase, onClose]);
|
|
71
|
+
|
|
72
|
+
const handleRestore = useCallback(async () => {
|
|
73
|
+
if (isProcessing) return;
|
|
74
|
+
setIsProcessing(true);
|
|
75
|
+
try {
|
|
76
|
+
const success = await onRestore();
|
|
77
|
+
if (success) onClose();
|
|
78
|
+
} finally {
|
|
79
|
+
setIsProcessing(false);
|
|
80
|
+
}
|
|
81
|
+
}, [isProcessing, onRestore, onClose]);
|
|
82
|
+
|
|
83
|
+
if (!visible) return null;
|
|
84
|
+
|
|
85
|
+
const hasPackages = packages.length > 0;
|
|
86
|
+
const showLoading = isLoading && !hasPackages;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
|
|
90
|
+
<View style={styles.overlay}>
|
|
91
|
+
<TouchableOpacity style={styles.backdrop} activeOpacity={1} onPress={onClose} />
|
|
92
|
+
<View style={[styles.content, { backgroundColor: tokens.colors.surface }]}>
|
|
93
|
+
<SafeAreaView edges={["bottom"]} style={styles.safeArea}>
|
|
94
|
+
<View style={styles.header}>
|
|
95
|
+
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
|
96
|
+
<AtomicText style={[styles.closeIcon, { color: tokens.colors.textSecondary }]}>
|
|
97
|
+
×
|
|
98
|
+
</AtomicText>
|
|
99
|
+
</TouchableOpacity>
|
|
100
|
+
<AtomicText
|
|
101
|
+
type="headlineMedium"
|
|
102
|
+
style={[styles.title, { color: tokens.colors.textPrimary }]}
|
|
103
|
+
>
|
|
104
|
+
{title}
|
|
105
|
+
</AtomicText>
|
|
106
|
+
{subtitle ? (
|
|
107
|
+
<AtomicText
|
|
108
|
+
type="bodyMedium"
|
|
109
|
+
style={[styles.subtitle, { color: tokens.colors.textSecondary }]}
|
|
110
|
+
>
|
|
111
|
+
{subtitle}
|
|
112
|
+
</AtomicText>
|
|
113
|
+
) : null}
|
|
114
|
+
</View>
|
|
115
|
+
|
|
116
|
+
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent}>
|
|
117
|
+
{showLoading ? (
|
|
118
|
+
<View style={styles.centerContent}>
|
|
119
|
+
<ActivityIndicator size="large" color={tokens.colors.primary} />
|
|
120
|
+
<AtomicText
|
|
121
|
+
type="bodyMedium"
|
|
122
|
+
style={[styles.loadingText, { color: tokens.colors.textSecondary }]}
|
|
123
|
+
>
|
|
124
|
+
{loadingText}
|
|
125
|
+
</AtomicText>
|
|
126
|
+
</View>
|
|
127
|
+
) : !hasPackages ? (
|
|
128
|
+
<View style={styles.centerContent}>
|
|
129
|
+
<AtomicText
|
|
130
|
+
type="bodyMedium"
|
|
131
|
+
style={[styles.emptyText, { color: tokens.colors.textSecondary }]}
|
|
132
|
+
>
|
|
133
|
+
{emptyText}
|
|
134
|
+
</AtomicText>
|
|
135
|
+
</View>
|
|
136
|
+
) : (
|
|
137
|
+
<View style={styles.packagesContainer}>
|
|
138
|
+
{packages.map((pkg, index) => (
|
|
139
|
+
<SubscriptionPlanCard
|
|
140
|
+
key={pkg.product.identifier}
|
|
141
|
+
package={pkg}
|
|
142
|
+
isSelected={selectedPkg?.product.identifier === pkg.product.identifier}
|
|
143
|
+
onSelect={() => setSelectedPkg(pkg)}
|
|
144
|
+
isBestValue={index === 0}
|
|
145
|
+
/>
|
|
146
|
+
))}
|
|
147
|
+
</View>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{features.length > 0 && (
|
|
151
|
+
<View style={[styles.featuresSection, { backgroundColor: tokens.colors.surfaceSecondary }]}>
|
|
152
|
+
<PaywallFeaturesList features={features} gap={12} />
|
|
153
|
+
</View>
|
|
154
|
+
)}
|
|
155
|
+
</ScrollView>
|
|
156
|
+
|
|
157
|
+
<View style={styles.footer}>
|
|
158
|
+
{hasPackages && (
|
|
159
|
+
<AtomicButton
|
|
160
|
+
title={isProcessing ? processingText : purchaseButtonText}
|
|
161
|
+
onPress={handlePurchase}
|
|
162
|
+
disabled={!selectedPkg || isProcessing || isLoading}
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
165
|
+
<TouchableOpacity
|
|
166
|
+
style={styles.restoreButton}
|
|
167
|
+
onPress={handleRestore}
|
|
168
|
+
disabled={isProcessing || isLoading}
|
|
169
|
+
>
|
|
170
|
+
<AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
|
|
171
|
+
{restoreButtonText}
|
|
172
|
+
</AtomicText>
|
|
173
|
+
</TouchableOpacity>
|
|
174
|
+
</View>
|
|
175
|
+
|
|
176
|
+
<PaywallLegalFooter />
|
|
177
|
+
</SafeAreaView>
|
|
178
|
+
</View>
|
|
179
|
+
</View>
|
|
180
|
+
</Modal>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
SubscriptionModal.displayName = "SubscriptionModal";
|
|
186
|
+
|
|
187
|
+
const styles = StyleSheet.create({
|
|
188
|
+
overlay: { flex: 1, justifyContent: "flex-end" },
|
|
189
|
+
backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
|
190
|
+
content: { borderTopLeftRadius: 24, borderTopRightRadius: 24, maxHeight: "90%" },
|
|
191
|
+
safeArea: { paddingTop: 16 },
|
|
192
|
+
header: { alignItems: "center", paddingHorizontal: 24, paddingBottom: 20 },
|
|
193
|
+
closeButton: { position: "absolute", top: 0, right: 16, padding: 8, zIndex: 1 },
|
|
194
|
+
closeIcon: { fontSize: 28, fontWeight: "300" },
|
|
195
|
+
title: { marginBottom: 8, textAlign: "center" },
|
|
196
|
+
subtitle: { textAlign: "center" },
|
|
197
|
+
scrollView: { maxHeight: 400 },
|
|
198
|
+
scrollContent: { paddingHorizontal: 24, paddingBottom: 16 },
|
|
199
|
+
centerContent: { alignItems: "center", paddingVertical: 40 },
|
|
200
|
+
loadingText: { marginTop: 16 },
|
|
201
|
+
emptyText: { textAlign: "center" },
|
|
202
|
+
packagesContainer: { gap: 12, marginBottom: 20 },
|
|
203
|
+
featuresSection: { borderRadius: 16, padding: 16 },
|
|
204
|
+
footer: { paddingHorizontal: 24, paddingVertical: 16, gap: 12 },
|
|
205
|
+
restoreButton: { alignItems: "center", paddingVertical: 8 },
|
|
206
|
+
});
|