@umituz/react-native-subscription 2.2.8 → 2.2.11
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/CreditsPackageCard.tsx +1 -1
- package/src/presentation/components/paywall/CreditsTabContent.tsx +2 -2
- package/src/presentation/components/paywall/PaywallModal.tsx +27 -9
- package/src/presentation/components/paywall/PaywallTabBar.tsx +1 -1
- package/src/presentation/components/paywall/SubscriptionModal.tsx +18 -4
- package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +1 -1
- package/src/presentation/components/paywall/SubscriptionTabContent.tsx +44 -44
- package/src/presentation/hooks/usePaywall.ts +54 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.11",
|
|
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",
|
|
@@ -58,4 +58,4 @@
|
|
|
58
58
|
"README.md",
|
|
59
59
|
"LICENSE"
|
|
60
60
|
]
|
|
61
|
-
}
|
|
61
|
+
}
|
|
@@ -7,7 +7,7 @@ import React from "react";
|
|
|
7
7
|
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
8
8
|
import { AtomicText } from "@umituz/react-native-design-system-atoms";
|
|
9
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
|
-
import type { CreditsPackage } from "
|
|
10
|
+
import type { CreditsPackage } from "../../../domain/entities/paywall/CreditsPackage";
|
|
11
11
|
|
|
12
12
|
interface CreditsPackageCardProps {
|
|
13
13
|
package: CreditsPackage;
|
|
@@ -10,7 +10,7 @@ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
|
10
10
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
11
11
|
import { CreditsPackageCard } from "./CreditsPackageCard";
|
|
12
12
|
import { PaywallLegalFooter } from "./PaywallLegalFooter";
|
|
13
|
-
import type { CreditsPackage } from "
|
|
13
|
+
import type { CreditsPackage } from "../../../domain/entities/paywall/CreditsPackage";
|
|
14
14
|
|
|
15
15
|
interface CreditsTabContentProps {
|
|
16
16
|
packages: CreditsPackage[];
|
|
@@ -42,7 +42,7 @@ export const CreditsTabContent: React.FC<CreditsTabContentProps> = React.memo(
|
|
|
42
42
|
const { t } = useLocalization();
|
|
43
43
|
|
|
44
44
|
const needsCredits = requiredCredits && requiredCredits > currentCredits;
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
const displayPurchaseButtonText = purchaseButtonText ||
|
|
47
47
|
t("paywall.purchase", { defaultValue: "Purchase" });
|
|
48
48
|
const displayProcessingText = processingText ||
|
|
@@ -9,13 +9,13 @@ import { SafeAreaView } from "react-native-safe-area-context";
|
|
|
9
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
10
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
11
11
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
12
|
-
import { usePaywall } from "
|
|
12
|
+
import { usePaywall } from "../../hooks/usePaywall";
|
|
13
13
|
import { PaywallHeader } from "./PaywallHeader";
|
|
14
14
|
import { PaywallTabBar } from "./PaywallTabBar";
|
|
15
15
|
import { CreditsTabContent } from "./CreditsTabContent";
|
|
16
16
|
import { SubscriptionTabContent } from "./SubscriptionTabContent";
|
|
17
|
-
import type { PaywallTabType } from "
|
|
18
|
-
import type { CreditsPackage } from "
|
|
17
|
+
import type { PaywallTabType } from "../../../domain/entities/paywall/PaywallTab";
|
|
18
|
+
import type { CreditsPackage } from "../../../domain/entities/paywall/CreditsPackage";
|
|
19
19
|
|
|
20
20
|
interface PaywallModalProps {
|
|
21
21
|
visible: boolean;
|
|
@@ -27,10 +27,16 @@ interface PaywallModalProps {
|
|
|
27
27
|
requiredCredits?: number;
|
|
28
28
|
onCreditsPurchase: (packageId: string) => Promise<void>;
|
|
29
29
|
onSubscriptionPurchase: (pkg: PurchasesPackage) => Promise<void>;
|
|
30
|
+
onRestore?: () => Promise<void>;
|
|
30
31
|
subscriptionFeatures?: Array<{ icon: string; text: string }>;
|
|
31
32
|
isLoading?: boolean;
|
|
32
33
|
title?: string;
|
|
33
34
|
subtitle?: string;
|
|
35
|
+
privacyUrl?: string;
|
|
36
|
+
termsUrl?: string;
|
|
37
|
+
privacyText?: string;
|
|
38
|
+
termsOfServiceText?: string;
|
|
39
|
+
restoreButtonText?: string;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
export const PaywallModal: React.FC<PaywallModalProps> = React.memo(
|
|
@@ -44,10 +50,16 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo(
|
|
|
44
50
|
requiredCredits,
|
|
45
51
|
onCreditsPurchase,
|
|
46
52
|
onSubscriptionPurchase,
|
|
53
|
+
onRestore,
|
|
47
54
|
subscriptionFeatures = [],
|
|
48
55
|
isLoading = false,
|
|
49
56
|
title,
|
|
50
57
|
subtitle,
|
|
58
|
+
privacyUrl,
|
|
59
|
+
termsUrl,
|
|
60
|
+
privacyText,
|
|
61
|
+
termsOfServiceText,
|
|
62
|
+
restoreButtonText,
|
|
51
63
|
}) => {
|
|
52
64
|
const tokens = useAppDesignTokens();
|
|
53
65
|
const { t } = useLocalization();
|
|
@@ -101,12 +113,12 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo(
|
|
|
101
113
|
onClose={onClose}
|
|
102
114
|
/>
|
|
103
115
|
|
|
104
|
-
<PaywallTabBar
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
<PaywallTabBar
|
|
117
|
+
activeTab={activeTab}
|
|
118
|
+
onTabChange={handleTabChange}
|
|
119
|
+
creditsLabel={t("paywall.tabs.credits", { defaultValue: "Credits" })}
|
|
120
|
+
subscriptionLabel={t("paywall.tabs.subscription", { defaultValue: "Subscription" })}
|
|
121
|
+
/>
|
|
110
122
|
|
|
111
123
|
{activeTab === "credits" ? (
|
|
112
124
|
<CreditsTabContent
|
|
@@ -128,6 +140,12 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo(
|
|
|
128
140
|
features={subscriptionFeatures}
|
|
129
141
|
isLoading={isLoading}
|
|
130
142
|
purchaseButtonText={t("paywall.subscribe", { defaultValue: "Subscribe" })}
|
|
143
|
+
onRestore={onRestore}
|
|
144
|
+
privacyUrl={privacyUrl}
|
|
145
|
+
termsUrl={termsUrl}
|
|
146
|
+
privacyText={privacyText}
|
|
147
|
+
termsOfServiceText={termsOfServiceText}
|
|
148
|
+
restoreButtonText={restoreButtonText}
|
|
131
149
|
/>
|
|
132
150
|
)}
|
|
133
151
|
</SafeAreaView>
|
|
@@ -7,7 +7,7 @@ import React from "react";
|
|
|
7
7
|
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
8
|
import { AtomicText } from "@umituz/react-native-design-system-atoms";
|
|
9
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
|
-
import type { PaywallTabType } from "
|
|
10
|
+
import type { PaywallTabType } from "../../../domain/entities/paywall/PaywallTab";
|
|
11
11
|
|
|
12
12
|
interface PaywallTabBarProps {
|
|
13
13
|
activeTab: PaywallTabType;
|
|
@@ -95,8 +95,13 @@ export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo(
|
|
|
95
95
|
|
|
96
96
|
if (!visible) return null;
|
|
97
97
|
|
|
98
|
+
const isFullScreen = variant === "fullscreen";
|
|
99
|
+
|
|
98
100
|
const Content = (
|
|
99
|
-
<SafeAreaView
|
|
101
|
+
<SafeAreaView
|
|
102
|
+
edges={["top", "bottom"]}
|
|
103
|
+
style={isFullScreen ? styles.safeAreaFullScreen : styles.safeAreaBottomSheet}
|
|
104
|
+
>
|
|
100
105
|
<View style={styles.header}>
|
|
101
106
|
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
|
102
107
|
<AtomicText style={[styles.closeIcon, { color: tokens.colors.textSecondary }]}>
|
|
@@ -119,7 +124,10 @@ export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo(
|
|
|
119
124
|
)}
|
|
120
125
|
</View>
|
|
121
126
|
|
|
122
|
-
<ScrollView
|
|
127
|
+
<ScrollView
|
|
128
|
+
style={isFullScreen ? styles.scrollViewFullScreen : styles.scrollViewBottomSheet}
|
|
129
|
+
contentContainerStyle={styles.scrollContent}
|
|
130
|
+
>
|
|
123
131
|
<SubscriptionPackageList
|
|
124
132
|
packages={packages}
|
|
125
133
|
isLoading={isLoading}
|
|
@@ -186,13 +194,19 @@ const styles = StyleSheet.create({
|
|
|
186
194
|
overlay: { flex: 1, justifyContent: "flex-end" },
|
|
187
195
|
backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: "rgba(0, 0, 0, 0.5)" },
|
|
188
196
|
bottomSheetContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, maxHeight: "90%" },
|
|
189
|
-
|
|
197
|
+
|
|
198
|
+
safeAreaFullScreen: { flex: 1, paddingTop: 16 },
|
|
199
|
+
safeAreaBottomSheet: { paddingTop: 16 },
|
|
200
|
+
|
|
190
201
|
header: { alignItems: "center", paddingHorizontal: 24, paddingBottom: 20 },
|
|
191
202
|
closeButton: { position: "absolute", top: 0, right: 16, padding: 8, zIndex: 1 },
|
|
192
203
|
closeIcon: { fontSize: 28, fontWeight: "300" },
|
|
193
204
|
title: { marginBottom: 8, textAlign: "center" },
|
|
194
205
|
subtitle: { textAlign: "center" },
|
|
195
|
-
|
|
206
|
+
|
|
207
|
+
scrollViewFullScreen: { flex: 1 },
|
|
208
|
+
scrollViewBottomSheet: { maxHeight: 400 },
|
|
209
|
+
|
|
196
210
|
scrollContent: { paddingHorizontal: 24, paddingBottom: 16 },
|
|
197
211
|
featuresSection: { borderRadius: 16, padding: 16, marginTop: 20 },
|
|
198
212
|
});
|
|
@@ -8,7 +8,7 @@ import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
|
8
8
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
9
9
|
import { AtomicText } from "@umituz/react-native-design-system-atoms";
|
|
10
10
|
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
11
|
-
import { formatPrice } from "
|
|
11
|
+
import { formatPrice } from "../../../utils/priceUtils";
|
|
12
12
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
13
13
|
|
|
14
14
|
interface SubscriptionPlanCardProps {
|
|
@@ -5,23 +5,28 @@
|
|
|
5
5
|
|
|
6
6
|
import React, { useMemo } from "react";
|
|
7
7
|
import { View, StyleSheet, ScrollView } from "react-native";
|
|
8
|
-
import { AtomicButton } from "@umituz/react-native-design-system-atoms";
|
|
9
8
|
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
9
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
11
10
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
12
|
-
import { SubscriptionPlanCard } from "./SubscriptionPlanCard";
|
|
13
11
|
import { PaywallFeaturesList } from "./PaywallFeaturesList";
|
|
14
|
-
import {
|
|
12
|
+
import { SubscriptionPackageList } from "./SubscriptionPackageList";
|
|
13
|
+
import { SubscriptionFooter } from "./SubscriptionFooter";
|
|
15
14
|
|
|
16
15
|
interface SubscriptionTabContentProps {
|
|
17
16
|
packages: PurchasesPackage[];
|
|
18
17
|
selectedPackage: PurchasesPackage | null;
|
|
19
18
|
onSelectPackage: (pkg: PurchasesPackage) => void;
|
|
20
19
|
onPurchase: () => void;
|
|
20
|
+
onRestore?: () => void;
|
|
21
21
|
features?: Array<{ icon: string; text: string }>;
|
|
22
22
|
isLoading?: boolean;
|
|
23
23
|
purchaseButtonText?: string;
|
|
24
24
|
processingText?: string;
|
|
25
|
+
restoreButtonText?: string;
|
|
26
|
+
privacyUrl?: string;
|
|
27
|
+
termsUrl?: string;
|
|
28
|
+
privacyText?: string;
|
|
29
|
+
termsOfServiceText?: string;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
const isYearlyPackage = (pkg: PurchasesPackage): boolean => {
|
|
@@ -46,26 +51,27 @@ export const SubscriptionTabContent: React.FC<SubscriptionTabContentProps> =
|
|
|
46
51
|
selectedPackage,
|
|
47
52
|
onSelectPackage,
|
|
48
53
|
onPurchase,
|
|
54
|
+
onRestore,
|
|
49
55
|
features = [],
|
|
50
56
|
isLoading = false,
|
|
51
57
|
purchaseButtonText,
|
|
52
58
|
processingText,
|
|
59
|
+
restoreButtonText = "Restore Purchases",
|
|
60
|
+
privacyUrl,
|
|
61
|
+
termsUrl,
|
|
62
|
+
privacyText,
|
|
63
|
+
termsOfServiceText,
|
|
53
64
|
}) => {
|
|
54
65
|
const tokens = useAppDesignTokens();
|
|
55
66
|
const { t } = useLocalization();
|
|
56
67
|
|
|
57
|
-
const displayPurchaseButtonText =
|
|
58
|
-
t("paywall.subscribe", { defaultValue: "Subscribe" });
|
|
59
|
-
const displayProcessingText =
|
|
60
|
-
t("paywall.processing", { defaultValue: "Processing..." });
|
|
68
|
+
const displayPurchaseButtonText =
|
|
69
|
+
purchaseButtonText || t("paywall.subscribe", { defaultValue: "Subscribe" });
|
|
70
|
+
const displayProcessingText =
|
|
71
|
+
processingText || t("paywall.processing", { defaultValue: "Processing..." });
|
|
61
72
|
|
|
62
73
|
const sortedPackages = useMemo(() => sortPackages(packages), [packages]);
|
|
63
74
|
|
|
64
|
-
const firstYearlyIndex = useMemo(
|
|
65
|
-
() => sortedPackages.findIndex(isYearlyPackage),
|
|
66
|
-
[sortedPackages],
|
|
67
|
-
);
|
|
68
|
-
|
|
69
75
|
return (
|
|
70
76
|
<View style={styles.container}>
|
|
71
77
|
<ScrollView
|
|
@@ -73,20 +79,14 @@ export const SubscriptionTabContent: React.FC<SubscriptionTabContentProps> =
|
|
|
73
79
|
contentContainerStyle={styles.scrollContent}
|
|
74
80
|
showsVerticalScrollIndicator={false}
|
|
75
81
|
>
|
|
76
|
-
<
|
|
77
|
-
{sortedPackages
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
onSelect={() => onSelectPackage(pkg)}
|
|
86
|
-
isBestValue={index === firstYearlyIndex}
|
|
87
|
-
/>
|
|
88
|
-
))}
|
|
89
|
-
</View>
|
|
82
|
+
<SubscriptionPackageList
|
|
83
|
+
packages={sortedPackages}
|
|
84
|
+
isLoading={isLoading}
|
|
85
|
+
selectedPkg={selectedPackage}
|
|
86
|
+
onSelect={onSelectPackage}
|
|
87
|
+
loadingText={t("paywall.loading", { defaultValue: "Loading..." })}
|
|
88
|
+
emptyText={t("paywall.empty", { defaultValue: "No packages" })}
|
|
89
|
+
/>
|
|
90
90
|
|
|
91
91
|
{features.length > 0 && (
|
|
92
92
|
<View
|
|
@@ -100,18 +100,25 @@ export const SubscriptionTabContent: React.FC<SubscriptionTabContentProps> =
|
|
|
100
100
|
)}
|
|
101
101
|
</ScrollView>
|
|
102
102
|
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
103
|
+
<SubscriptionFooter
|
|
104
|
+
isProcessing={false} // Tab content usually delegated processing to external state, currently no isProcessing prop passed. Assuming false or controlled by isLoading.
|
|
105
|
+
isLoading={isLoading}
|
|
106
|
+
processingText={displayProcessingText}
|
|
107
|
+
purchaseButtonText={displayPurchaseButtonText}
|
|
108
|
+
hasPackages={packages.length > 0}
|
|
109
|
+
selectedPkg={selectedPackage}
|
|
110
|
+
restoreButtonText={restoreButtonText}
|
|
111
|
+
showRestoreButton={!!onRestore}
|
|
112
|
+
onPurchase={onPurchase}
|
|
113
|
+
onRestore={onRestore || (() => { })}
|
|
114
|
+
privacyUrl={privacyUrl}
|
|
115
|
+
termsUrl={termsUrl}
|
|
116
|
+
privacyText={privacyText}
|
|
117
|
+
termsOfServiceText={termsOfServiceText}
|
|
118
|
+
/>
|
|
112
119
|
</View>
|
|
113
120
|
);
|
|
114
|
-
}
|
|
121
|
+
}
|
|
115
122
|
);
|
|
116
123
|
|
|
117
124
|
SubscriptionTabContent.displayName = "SubscriptionTabContent";
|
|
@@ -127,16 +134,9 @@ const styles = StyleSheet.create({
|
|
|
127
134
|
paddingHorizontal: 24,
|
|
128
135
|
paddingBottom: 16,
|
|
129
136
|
},
|
|
130
|
-
plansContainer: {
|
|
131
|
-
gap: 12,
|
|
132
|
-
marginBottom: 20,
|
|
133
|
-
},
|
|
134
137
|
featuresSection: {
|
|
135
138
|
borderRadius: 16,
|
|
136
139
|
padding: 16,
|
|
137
|
-
|
|
138
|
-
footer: {
|
|
139
|
-
padding: 24,
|
|
140
|
-
paddingTop: 16,
|
|
140
|
+
marginTop: 20,
|
|
141
141
|
},
|
|
142
142
|
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
3
|
+
import type { PaywallTabType } from "../../domain/entities/paywall/PaywallTab";
|
|
4
|
+
|
|
5
|
+
interface UsePaywallProps {
|
|
6
|
+
initialTab?: PaywallTabType;
|
|
7
|
+
onCreditsPurchase: (packageId: string) => Promise<void>;
|
|
8
|
+
onSubscriptionPurchase: (pkg: PurchasesPackage) => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const usePaywall = ({
|
|
12
|
+
initialTab = "credits",
|
|
13
|
+
onCreditsPurchase,
|
|
14
|
+
onSubscriptionPurchase,
|
|
15
|
+
}: UsePaywallProps) => {
|
|
16
|
+
const [activeTab, setActiveTab] = useState<PaywallTabType>(initialTab);
|
|
17
|
+
const [selectedCreditsPackageId, setSelectedCreditsPackageId] = useState<string | null>(null);
|
|
18
|
+
const [selectedSubscriptionPkg, setSelectedSubscriptionPkg] = useState<PurchasesPackage | null>(null);
|
|
19
|
+
|
|
20
|
+
const handleTabChange = useCallback((tab: PaywallTabType) => {
|
|
21
|
+
setActiveTab(tab);
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const handleCreditsPackageSelect = useCallback((packageId: string) => {
|
|
25
|
+
setSelectedCreditsPackageId(packageId);
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const handleSubscriptionPackageSelect = useCallback((pkg: PurchasesPackage) => {
|
|
29
|
+
setSelectedSubscriptionPkg(pkg);
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const handleCreditsPurchase = useCallback(async () => {
|
|
33
|
+
if (selectedCreditsPackageId) {
|
|
34
|
+
await onCreditsPurchase(selectedCreditsPackageId);
|
|
35
|
+
}
|
|
36
|
+
}, [selectedCreditsPackageId, onCreditsPurchase]);
|
|
37
|
+
|
|
38
|
+
const handleSubscriptionPurchase = useCallback(async () => {
|
|
39
|
+
if (selectedSubscriptionPkg) {
|
|
40
|
+
await onSubscriptionPurchase(selectedSubscriptionPkg);
|
|
41
|
+
}
|
|
42
|
+
}, [selectedSubscriptionPkg, onSubscriptionPurchase]);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
activeTab,
|
|
46
|
+
selectedCreditsPackageId,
|
|
47
|
+
selectedSubscriptionPkg,
|
|
48
|
+
handleTabChange,
|
|
49
|
+
handleCreditsPackageSelect,
|
|
50
|
+
handleSubscriptionPackageSelect,
|
|
51
|
+
handleCreditsPurchase,
|
|
52
|
+
handleSubscriptionPurchase,
|
|
53
|
+
};
|
|
54
|
+
};
|