@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.
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Subscription Plan Card Component
3
+ * Single Responsibility: Display a subscription plan option
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, StyleSheet } from "react-native";
8
+ import type { PurchasesPackage } from "react-native-purchases";
9
+ import { AtomicText } from "@umituz/react-native-design-system-atoms";
10
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
11
+ import { formatPrice } from "@umituz/react-native-subscription";
12
+ import { useLocalization } from "@umituz/react-native-localization";
13
+
14
+ interface SubscriptionPlanCardProps {
15
+ package: PurchasesPackage;
16
+ isSelected: boolean;
17
+ onSelect: () => void;
18
+ isBestValue?: boolean;
19
+ }
20
+
21
+ const getPeriodLabel = (period: string | null | undefined): string => {
22
+ if (!period) return "";
23
+ if (period.includes("Y") || period.includes("year")) return "yearly";
24
+ if (period.includes("M") || period.includes("month")) return "monthly";
25
+ if (period.includes("W") || period.includes("week")) return "weekly";
26
+ if (period.includes("D") || period.includes("day")) return "daily";
27
+ return "";
28
+ };
29
+
30
+ const isYearlyPeriod = (period: string | null | undefined): boolean => {
31
+ return period?.includes("Y") || period?.includes("year") || false;
32
+ };
33
+
34
+ export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
35
+ React.memo(({ package: pkg, isSelected, onSelect, isBestValue = false }) => {
36
+ const tokens = useAppDesignTokens();
37
+ const { t } = useLocalization();
38
+
39
+ const period = pkg.product.subscriptionPeriod;
40
+ const isYearly = isYearlyPeriod(period);
41
+ const periodLabel = getPeriodLabel(period);
42
+ const price = formatPrice(pkg.product.price, pkg.product.currencyCode);
43
+ const monthlyEquivalent = isYearly
44
+ ? formatPrice(pkg.product.price / 12, pkg.product.currencyCode)
45
+ : null;
46
+
47
+ const title = pkg.product.title || t(`paywall.${periodLabel}`, { defaultValue: periodLabel });
48
+
49
+ return (
50
+ <TouchableOpacity
51
+ onPress={onSelect}
52
+ activeOpacity={0.8}
53
+ style={[
54
+ styles.container,
55
+ {
56
+ backgroundColor: isSelected
57
+ ? tokens.colors.primaryLight
58
+ : tokens.colors.surface,
59
+ borderColor: isSelected
60
+ ? tokens.colors.primary
61
+ : tokens.colors.border,
62
+ borderWidth: isSelected ? 2 : 1,
63
+ },
64
+ ]}
65
+ >
66
+ {isBestValue && (
67
+ <View
68
+ style={[styles.badge, { backgroundColor: tokens.colors.primary }]}
69
+ >
70
+ <AtomicText
71
+ type="labelSmall"
72
+ style={{ color: tokens.colors.onPrimary, fontWeight: "600" }}
73
+ >
74
+ {t("paywall.bestValue", { defaultValue: "Best Value" })}
75
+ </AtomicText>
76
+ </View>
77
+ )}
78
+
79
+ <View style={styles.content}>
80
+ <View style={styles.leftSection}>
81
+ <View
82
+ style={[
83
+ styles.radio,
84
+ {
85
+ borderColor: isSelected
86
+ ? tokens.colors.primary
87
+ : tokens.colors.border,
88
+ },
89
+ ]}
90
+ >
91
+ {isSelected && (
92
+ <View
93
+ style={[
94
+ styles.radioInner,
95
+ { backgroundColor: tokens.colors.primary },
96
+ ]}
97
+ />
98
+ )}
99
+ </View>
100
+ <View style={styles.textContainer}>
101
+ <AtomicText
102
+ type="titleMedium"
103
+ style={[styles.title, { color: tokens.colors.textPrimary }]}
104
+ >
105
+ {title}
106
+ </AtomicText>
107
+ {isYearly && (
108
+ <AtomicText
109
+ type="bodySmall"
110
+ style={{ color: tokens.colors.textSecondary }}
111
+ >
112
+ {price}
113
+ </AtomicText>
114
+ )}
115
+ </View>
116
+ </View>
117
+
118
+ <View style={styles.rightSection}>
119
+ <AtomicText
120
+ type="titleMedium"
121
+ style={[styles.price, { color: tokens.colors.textPrimary }]}
122
+ >
123
+ {isYearly && monthlyEquivalent
124
+ ? `${monthlyEquivalent}/mo`
125
+ : price}
126
+ </AtomicText>
127
+ </View>
128
+ </View>
129
+ </TouchableOpacity>
130
+ );
131
+ });
132
+
133
+ SubscriptionPlanCard.displayName = "SubscriptionPlanCard";
134
+
135
+ const styles = StyleSheet.create({
136
+ container: {
137
+ borderRadius: 16,
138
+ padding: 18,
139
+ position: "relative",
140
+ },
141
+ badge: {
142
+ position: "absolute",
143
+ top: -10,
144
+ right: 20,
145
+ paddingHorizontal: 12,
146
+ paddingVertical: 4,
147
+ borderRadius: 8,
148
+ },
149
+ content: {
150
+ flexDirection: "row",
151
+ justifyContent: "space-between",
152
+ alignItems: "center",
153
+ },
154
+ leftSection: {
155
+ flexDirection: "row",
156
+ alignItems: "center",
157
+ flex: 1,
158
+ },
159
+ radio: {
160
+ width: 24,
161
+ height: 24,
162
+ borderRadius: 12,
163
+ borderWidth: 2,
164
+ alignItems: "center",
165
+ justifyContent: "center",
166
+ marginRight: 16,
167
+ },
168
+ radioInner: {
169
+ width: 12,
170
+ height: 12,
171
+ borderRadius: 6,
172
+ },
173
+ textContainer: {
174
+ flex: 1,
175
+ },
176
+ title: {
177
+ fontWeight: "600",
178
+ marginBottom: 2,
179
+ },
180
+ rightSection: {
181
+ alignItems: "flex-end",
182
+ },
183
+ price: {
184
+ fontWeight: "700",
185
+ },
186
+ });
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Subscription Tab Content Component
3
+ * Single Responsibility: Display subscription plans list
4
+ */
5
+
6
+ import React, { useMemo } from "react";
7
+ import { View, StyleSheet, ScrollView } from "react-native";
8
+ import { AtomicButton } from "@umituz/react-native-design-system-atoms";
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 { SubscriptionPlanCard } from "./SubscriptionPlanCard";
13
+ import { PaywallFeaturesList } from "./PaywallFeaturesList";
14
+ import { PaywallLegalFooter } from "./PaywallLegalFooter";
15
+
16
+ interface SubscriptionTabContentProps {
17
+ packages: PurchasesPackage[];
18
+ selectedPackage: PurchasesPackage | null;
19
+ onSelectPackage: (pkg: PurchasesPackage) => void;
20
+ onPurchase: () => void;
21
+ features?: Array<{ icon: string; text: string }>;
22
+ isLoading?: boolean;
23
+ purchaseButtonText?: string;
24
+ processingText?: string;
25
+ }
26
+
27
+ const isYearlyPackage = (pkg: PurchasesPackage): boolean => {
28
+ const period = pkg.product.subscriptionPeriod;
29
+ return period?.includes("Y") || period?.includes("year") || false;
30
+ };
31
+
32
+ const sortPackages = (packages: PurchasesPackage[]): PurchasesPackage[] => {
33
+ return [...packages].sort((a, b) => {
34
+ const aIsYearly = isYearlyPackage(a);
35
+ const bIsYearly = isYearlyPackage(b);
36
+ if (aIsYearly && !bIsYearly) return -1;
37
+ if (!aIsYearly && bIsYearly) return 1;
38
+ return b.product.price - a.product.price;
39
+ });
40
+ };
41
+
42
+ export const SubscriptionTabContent: React.FC<SubscriptionTabContentProps> =
43
+ React.memo(
44
+ ({
45
+ packages,
46
+ selectedPackage,
47
+ onSelectPackage,
48
+ onPurchase,
49
+ features = [],
50
+ isLoading = false,
51
+ purchaseButtonText,
52
+ processingText,
53
+ }) => {
54
+ const tokens = useAppDesignTokens();
55
+ const { t } = useLocalization();
56
+
57
+ const displayPurchaseButtonText = purchaseButtonText ||
58
+ t("paywall.subscribe", { defaultValue: "Subscribe" });
59
+ const displayProcessingText = processingText ||
60
+ t("paywall.processing", { defaultValue: "Processing..." });
61
+
62
+ const sortedPackages = useMemo(() => sortPackages(packages), [packages]);
63
+
64
+ const firstYearlyIndex = useMemo(
65
+ () => sortedPackages.findIndex(isYearlyPackage),
66
+ [sortedPackages],
67
+ );
68
+
69
+ return (
70
+ <View style={styles.container}>
71
+ <ScrollView
72
+ style={styles.scrollView}
73
+ contentContainerStyle={styles.scrollContent}
74
+ showsVerticalScrollIndicator={false}
75
+ >
76
+ <View style={styles.plansContainer}>
77
+ {sortedPackages.map((pkg, index) => (
78
+ <SubscriptionPlanCard
79
+ key={pkg.product.identifier}
80
+ package={pkg}
81
+ isSelected={
82
+ selectedPackage?.product.identifier ===
83
+ pkg.product.identifier
84
+ }
85
+ onSelect={() => onSelectPackage(pkg)}
86
+ isBestValue={index === firstYearlyIndex}
87
+ />
88
+ ))}
89
+ </View>
90
+
91
+ {features.length > 0 && (
92
+ <View
93
+ style={[
94
+ styles.featuresSection,
95
+ { backgroundColor: tokens.colors.surfaceSecondary },
96
+ ]}
97
+ >
98
+ <PaywallFeaturesList features={features} gap={12} />
99
+ </View>
100
+ )}
101
+ </ScrollView>
102
+
103
+ <View style={styles.footer}>
104
+ <AtomicButton
105
+ title={isLoading ? displayProcessingText : displayPurchaseButtonText}
106
+ onPress={onPurchase}
107
+ disabled={!selectedPackage || isLoading}
108
+ />
109
+ </View>
110
+
111
+ <PaywallLegalFooter />
112
+ </View>
113
+ );
114
+ },
115
+ );
116
+
117
+ SubscriptionTabContent.displayName = "SubscriptionTabContent";
118
+
119
+ const styles = StyleSheet.create({
120
+ container: {
121
+ flex: 1,
122
+ },
123
+ scrollView: {
124
+ flex: 1,
125
+ },
126
+ scrollContent: {
127
+ paddingHorizontal: 24,
128
+ paddingBottom: 16,
129
+ },
130
+ plansContainer: {
131
+ gap: 12,
132
+ marginBottom: 20,
133
+ },
134
+ featuresSection: {
135
+ borderRadius: 16,
136
+ padding: 16,
137
+ },
138
+ footer: {
139
+ padding: 24,
140
+ paddingTop: 16,
141
+ },
142
+ });