@umituz/react-native-subscription 2.10.8 → 2.10.9
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 +3 -1
- package/src/presentation/components/paywall/BestValueBadge.tsx +10 -10
- package/src/presentation/components/paywall/PaywallLegalFooter.tsx +73 -26
- package/src/presentation/components/paywall/SubscriptionFooter.tsx +47 -16
- package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +66 -52
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.9",
|
|
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",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"@umituz/react-native-localization": "latest",
|
|
38
38
|
"@umituz/react-native-sentry": "latest",
|
|
39
39
|
"expo-constants": ">=16.0.0",
|
|
40
|
+
"expo-linear-gradient": ">=14.0.0",
|
|
40
41
|
"firebase": ">=10.0.0",
|
|
41
42
|
"react": ">=18.2.0",
|
|
42
43
|
"react-native": ">=0.74.0",
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
"@umituz/react-native-sentry": "latest",
|
|
52
53
|
"@tanstack/react-query": "^5.0.0",
|
|
53
54
|
"expo-constants": "~16.0.0",
|
|
55
|
+
"expo-linear-gradient": "~15.0.0",
|
|
54
56
|
"firebase": "^10.0.0",
|
|
55
57
|
"react-native-purchases": "^7.0.0",
|
|
56
58
|
"@types/react": "~19.1.10",
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Best Value Badge Component
|
|
3
|
-
* Single Responsibility: Display a "Best Value" badge for subscription packages
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import React from "react";
|
|
7
|
-
import {
|
|
2
|
+
import { StyleSheet } from "react-native";
|
|
8
3
|
import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
import { LinearGradient } from "expo-linear-gradient";
|
|
9
6
|
|
|
10
7
|
interface BestValueBadgeProps {
|
|
11
8
|
text: string;
|
|
@@ -19,16 +16,19 @@ export const BestValueBadge: React.FC<BestValueBadgeProps> = React.memo(
|
|
|
19
16
|
if (!visible) return null;
|
|
20
17
|
|
|
21
18
|
return (
|
|
22
|
-
<
|
|
23
|
-
|
|
19
|
+
<LinearGradient
|
|
20
|
+
colors={[tokens.colors.secondary, tokens.colors.primary]}
|
|
21
|
+
start={{ x: 0, y: 0 }}
|
|
22
|
+
end={{ x: 1, y: 1 }}
|
|
23
|
+
style={styles.badge}
|
|
24
24
|
>
|
|
25
25
|
<AtomicText
|
|
26
26
|
type="labelSmall"
|
|
27
|
-
style={{ color: tokens.colors.onPrimary, fontWeight: "
|
|
27
|
+
style={{ color: tokens.colors.onPrimary, fontWeight: "800", textTransform: "uppercase", fontSize: 10 }}
|
|
28
28
|
>
|
|
29
29
|
{text}
|
|
30
30
|
</AtomicText>
|
|
31
|
-
</
|
|
31
|
+
</LinearGradient>
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
);
|
|
@@ -14,6 +14,10 @@ interface PaywallLegalFooterProps {
|
|
|
14
14
|
termsUrl?: string;
|
|
15
15
|
privacyText?: string;
|
|
16
16
|
termsOfServiceText?: string;
|
|
17
|
+
showRestoreButton?: boolean;
|
|
18
|
+
restoreButtonText?: string;
|
|
19
|
+
onRestore?: () => void;
|
|
20
|
+
isProcessing?: boolean;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
const DEFAULT_TERMS =
|
|
@@ -26,18 +30,13 @@ export const PaywallLegalFooter: React.FC<PaywallLegalFooterProps> = React.memo(
|
|
|
26
30
|
termsUrl,
|
|
27
31
|
privacyText = "Privacy Policy",
|
|
28
32
|
termsOfServiceText = "Terms of Service",
|
|
33
|
+
showRestoreButton = false,
|
|
34
|
+
restoreButtonText = "Restore Purchases",
|
|
35
|
+
onRestore,
|
|
36
|
+
isProcessing = false,
|
|
29
37
|
}) => {
|
|
30
38
|
const tokens = useAppDesignTokens();
|
|
31
39
|
|
|
32
|
-
if (__DEV__) {
|
|
33
|
-
console.log("[PaywallLegalFooter] Rendering links:", {
|
|
34
|
-
privacy: !!privacyUrl,
|
|
35
|
-
terms: !!termsUrl,
|
|
36
|
-
pText: privacyText,
|
|
37
|
-
tText: termsOfServiceText
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
40
|
const handlePrivacyPress = () => {
|
|
42
41
|
if (privacyUrl) {
|
|
43
42
|
Linking.openURL(privacyUrl).catch(console.error);
|
|
@@ -50,7 +49,7 @@ export const PaywallLegalFooter: React.FC<PaywallLegalFooterProps> = React.memo(
|
|
|
50
49
|
}
|
|
51
50
|
};
|
|
52
51
|
|
|
53
|
-
const hasLinks = privacyUrl || termsUrl;
|
|
52
|
+
const hasLinks = privacyUrl || termsUrl || showRestoreButton;
|
|
54
53
|
|
|
55
54
|
return (
|
|
56
55
|
<View style={styles.container}>
|
|
@@ -63,24 +62,69 @@ export const PaywallLegalFooter: React.FC<PaywallLegalFooterProps> = React.memo(
|
|
|
63
62
|
|
|
64
63
|
{hasLinks && (
|
|
65
64
|
<View style={styles.legalLinksWrapper}>
|
|
66
|
-
<View
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
65
|
+
<View
|
|
66
|
+
style={[
|
|
67
|
+
styles.legalLinksContainer,
|
|
68
|
+
{ borderColor: tokens.colors.border },
|
|
69
|
+
]}
|
|
70
|
+
>
|
|
71
|
+
{showRestoreButton && (
|
|
72
|
+
<>
|
|
73
|
+
<TouchableOpacity
|
|
74
|
+
onPress={onRestore}
|
|
75
|
+
activeOpacity={0.6}
|
|
76
|
+
style={styles.linkItem}
|
|
77
|
+
disabled={isProcessing}
|
|
76
78
|
>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
<AtomicText
|
|
80
|
+
type="labelSmall"
|
|
81
|
+
style={[
|
|
82
|
+
styles.linkText,
|
|
83
|
+
{
|
|
84
|
+
color: tokens.colors.textSecondary,
|
|
85
|
+
},
|
|
86
|
+
]}
|
|
87
|
+
>
|
|
88
|
+
{restoreButtonText}
|
|
89
|
+
</AtomicText>
|
|
90
|
+
</TouchableOpacity>
|
|
91
|
+
{(privacyUrl || termsUrl) && (
|
|
92
|
+
<View
|
|
93
|
+
style={[
|
|
94
|
+
styles.dot,
|
|
95
|
+
{ backgroundColor: tokens.colors.border },
|
|
96
|
+
]}
|
|
97
|
+
/>
|
|
98
|
+
)}
|
|
99
|
+
</>
|
|
80
100
|
)}
|
|
81
101
|
|
|
82
|
-
{privacyUrl &&
|
|
83
|
-
|
|
102
|
+
{privacyUrl && (
|
|
103
|
+
<>
|
|
104
|
+
<TouchableOpacity
|
|
105
|
+
onPress={handlePrivacyPress}
|
|
106
|
+
activeOpacity={0.6}
|
|
107
|
+
style={styles.linkItem}
|
|
108
|
+
>
|
|
109
|
+
<AtomicText
|
|
110
|
+
type="labelSmall"
|
|
111
|
+
style={[
|
|
112
|
+
styles.linkText,
|
|
113
|
+
{ color: tokens.colors.textSecondary },
|
|
114
|
+
]}
|
|
115
|
+
>
|
|
116
|
+
{privacyText}
|
|
117
|
+
</AtomicText>
|
|
118
|
+
</TouchableOpacity>
|
|
119
|
+
{termsUrl && (
|
|
120
|
+
<View
|
|
121
|
+
style={[
|
|
122
|
+
styles.dot,
|
|
123
|
+
{ backgroundColor: tokens.colors.border },
|
|
124
|
+
]}
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
</>
|
|
84
128
|
)}
|
|
85
129
|
|
|
86
130
|
{termsUrl && (
|
|
@@ -91,7 +135,10 @@ export const PaywallLegalFooter: React.FC<PaywallLegalFooterProps> = React.memo(
|
|
|
91
135
|
>
|
|
92
136
|
<AtomicText
|
|
93
137
|
type="labelSmall"
|
|
94
|
-
style={[
|
|
138
|
+
style={[
|
|
139
|
+
styles.linkText,
|
|
140
|
+
{ color: tokens.colors.textSecondary },
|
|
141
|
+
]}
|
|
95
142
|
>
|
|
96
143
|
{termsOfServiceText}
|
|
97
144
|
</AtomicText>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
3
|
-
import {
|
|
3
|
+
import { AtomicText } from "@umituz/react-native-design-system";
|
|
4
4
|
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
5
5
|
import { PaywallLegalFooter } from "./PaywallLegalFooter";
|
|
6
6
|
|
|
@@ -21,6 +21,9 @@ interface SubscriptionFooterProps {
|
|
|
21
21
|
onRestore: () => void;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
import { LinearGradient } from "expo-linear-gradient";
|
|
26
|
+
|
|
24
27
|
export const SubscriptionFooter: React.FC<SubscriptionFooterProps> = React.memo(
|
|
25
28
|
({
|
|
26
29
|
isProcessing,
|
|
@@ -40,25 +43,38 @@ export const SubscriptionFooter: React.FC<SubscriptionFooterProps> = React.memo(
|
|
|
40
43
|
}) => {
|
|
41
44
|
const tokens = useAppDesignTokens();
|
|
42
45
|
|
|
46
|
+
const isDisabled = !selectedPkg || isProcessing || isLoading;
|
|
47
|
+
|
|
43
48
|
return (
|
|
44
49
|
<View style={styles.container}>
|
|
45
50
|
<View style={styles.actions}>
|
|
46
51
|
{hasPackages && (
|
|
47
|
-
<AtomicButton
|
|
48
|
-
title={isProcessing ? processingText : purchaseButtonText}
|
|
49
|
-
onPress={onPurchase}
|
|
50
|
-
disabled={!selectedPkg || isProcessing || isLoading}
|
|
51
|
-
/>
|
|
52
|
-
)}
|
|
53
|
-
{showRestoreButton && (
|
|
54
52
|
<TouchableOpacity
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
onPress={onPurchase}
|
|
54
|
+
disabled={isDisabled}
|
|
55
|
+
activeOpacity={0.8}
|
|
58
56
|
>
|
|
59
|
-
<
|
|
60
|
-
{
|
|
61
|
-
|
|
57
|
+
<LinearGradient
|
|
58
|
+
colors={
|
|
59
|
+
isDisabled
|
|
60
|
+
? [tokens.colors.border, tokens.colors.borderLight]
|
|
61
|
+
: [tokens.colors.primary, tokens.colors.secondary]
|
|
62
|
+
}
|
|
63
|
+
start={{ x: 0, y: 0 }}
|
|
64
|
+
end={{ x: 1, y: 0 }}
|
|
65
|
+
style={[styles.gradientButton, isDisabled && { opacity: 0.6 }]}
|
|
66
|
+
>
|
|
67
|
+
<AtomicText
|
|
68
|
+
type="titleSmall"
|
|
69
|
+
style={{
|
|
70
|
+
color: tokens.colors.onPrimary,
|
|
71
|
+
fontWeight: "800",
|
|
72
|
+
fontSize: 16,
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
{isProcessing ? processingText : purchaseButtonText}
|
|
76
|
+
</AtomicText>
|
|
77
|
+
</LinearGradient>
|
|
62
78
|
</TouchableOpacity>
|
|
63
79
|
)}
|
|
64
80
|
</View>
|
|
@@ -68,6 +84,10 @@ export const SubscriptionFooter: React.FC<SubscriptionFooterProps> = React.memo(
|
|
|
68
84
|
termsUrl={termsUrl}
|
|
69
85
|
privacyText={privacyText}
|
|
70
86
|
termsOfServiceText={termsOfServiceText}
|
|
87
|
+
showRestoreButton={showRestoreButton}
|
|
88
|
+
restoreButtonText={restoreButtonText}
|
|
89
|
+
onRestore={onRestore}
|
|
90
|
+
isProcessing={isProcessing || isLoading}
|
|
71
91
|
/>
|
|
72
92
|
</View>
|
|
73
93
|
);
|
|
@@ -81,10 +101,21 @@ const styles = StyleSheet.create({
|
|
|
81
101
|
actions: {
|
|
82
102
|
paddingHorizontal: 24,
|
|
83
103
|
paddingVertical: 16,
|
|
84
|
-
gap: 12
|
|
104
|
+
gap: 12,
|
|
105
|
+
},
|
|
106
|
+
gradientButton: {
|
|
107
|
+
paddingVertical: 16,
|
|
108
|
+
borderRadius: 16,
|
|
109
|
+
alignItems: "center",
|
|
110
|
+
justifyContent: "center",
|
|
111
|
+
shadowColor: "#000",
|
|
112
|
+
shadowOffset: { width: 0, height: 4 },
|
|
113
|
+
shadowOpacity: 0.2,
|
|
114
|
+
shadowRadius: 8,
|
|
115
|
+
elevation: 4,
|
|
85
116
|
},
|
|
86
117
|
restoreButton: {
|
|
87
118
|
alignItems: "center",
|
|
88
|
-
paddingVertical: 8
|
|
119
|
+
paddingVertical: 8,
|
|
89
120
|
},
|
|
90
121
|
});
|
|
@@ -12,6 +12,9 @@ import { formatPrice } from "../../../utils/priceUtils";
|
|
|
12
12
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
13
13
|
import { BestValueBadge } from "./BestValueBadge";
|
|
14
14
|
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
import { LinearGradient } from "expo-linear-gradient";
|
|
17
|
+
|
|
15
18
|
interface SubscriptionPlanCardProps {
|
|
16
19
|
package: PurchasesPackage;
|
|
17
20
|
isSelected: boolean;
|
|
@@ -47,6 +50,15 @@ export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
|
47
50
|
|
|
48
51
|
const title = pkg.product.title || t(`paywall.period.${periodLabel}`);
|
|
49
52
|
|
|
53
|
+
const CardComponent = isSelected ? LinearGradient : View;
|
|
54
|
+
const cardProps = isSelected
|
|
55
|
+
? {
|
|
56
|
+
colors: [tokens.colors.primary + "20", tokens.colors.surface],
|
|
57
|
+
start: { x: 0, y: 0 },
|
|
58
|
+
end: { x: 1, y: 1 },
|
|
59
|
+
}
|
|
60
|
+
: {};
|
|
61
|
+
|
|
50
62
|
return (
|
|
51
63
|
<TouchableOpacity
|
|
52
64
|
onPress={onSelect}
|
|
@@ -54,82 +66,84 @@ export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
|
54
66
|
style={[
|
|
55
67
|
styles.container,
|
|
56
68
|
{
|
|
57
|
-
backgroundColor: isSelected
|
|
58
|
-
? tokens.colors.primaryLight
|
|
59
|
-
: tokens.colors.surface,
|
|
60
69
|
borderColor: isSelected
|
|
61
70
|
? tokens.colors.primary
|
|
62
|
-
: tokens.colors.
|
|
71
|
+
: tokens.colors.borderLight,
|
|
63
72
|
borderWidth: isSelected ? 2 : 1,
|
|
73
|
+
backgroundColor: isSelected ? undefined : tokens.colors.surface,
|
|
64
74
|
},
|
|
65
75
|
]}
|
|
66
76
|
>
|
|
67
|
-
<
|
|
68
|
-
text={t("paywall.bestValue")}
|
|
69
|
-
visible={isBestValue}
|
|
70
|
-
/>
|
|
77
|
+
<CardComponent {...(cardProps as any)} style={styles.gradientWrapper}>
|
|
78
|
+
<BestValueBadge text={t("paywall.bestValue")} visible={isBestValue} />
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
>
|
|
84
|
-
{isSelected && (
|
|
85
|
-
<View
|
|
86
|
-
style={[
|
|
87
|
-
styles.radioInner,
|
|
88
|
-
{ backgroundColor: tokens.colors.primary },
|
|
89
|
-
]}
|
|
90
|
-
/>
|
|
91
|
-
)}
|
|
92
|
-
</View>
|
|
93
|
-
<View style={styles.textContainer}>
|
|
94
|
-
<AtomicText
|
|
95
|
-
type="titleMedium"
|
|
96
|
-
style={[styles.title, { color: tokens.colors.textPrimary }]}
|
|
80
|
+
<View style={styles.content}>
|
|
81
|
+
<View style={styles.leftSection}>
|
|
82
|
+
<View
|
|
83
|
+
style={[
|
|
84
|
+
styles.radio,
|
|
85
|
+
{
|
|
86
|
+
borderColor: isSelected
|
|
87
|
+
? tokens.colors.primary
|
|
88
|
+
: tokens.colors.border,
|
|
89
|
+
},
|
|
90
|
+
]}
|
|
97
91
|
>
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
{isSelected && (
|
|
93
|
+
<View
|
|
94
|
+
style={[
|
|
95
|
+
styles.radioInner,
|
|
96
|
+
{ backgroundColor: tokens.colors.primary },
|
|
97
|
+
]}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
</View>
|
|
101
|
+
<View style={styles.textContainer}>
|
|
101
102
|
<AtomicText
|
|
102
|
-
type="
|
|
103
|
-
style={{ color: tokens.colors.
|
|
103
|
+
type="titleSmall"
|
|
104
|
+
style={[styles.title, { color: tokens.colors.textPrimary }]}
|
|
104
105
|
>
|
|
105
|
-
{
|
|
106
|
+
{title}
|
|
106
107
|
</AtomicText>
|
|
107
|
-
|
|
108
|
+
{isYearly && (
|
|
109
|
+
<AtomicText
|
|
110
|
+
type="bodySmall"
|
|
111
|
+
style={{ color: tokens.colors.textSecondary, fontSize: 11 }}
|
|
112
|
+
>
|
|
113
|
+
{price}
|
|
114
|
+
</AtomicText>
|
|
115
|
+
)}
|
|
116
|
+
</View>
|
|
108
117
|
</View>
|
|
109
|
-
</View>
|
|
110
118
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
<View style={styles.rightSection}>
|
|
120
|
+
<AtomicText
|
|
121
|
+
type="titleMedium"
|
|
122
|
+
style={[styles.price, { color: tokens.colors.textPrimary }]}
|
|
123
|
+
>
|
|
124
|
+
{isYearly && monthlyEquivalent
|
|
125
|
+
? `${monthlyEquivalent}/mo`
|
|
126
|
+
: price}
|
|
127
|
+
</AtomicText>
|
|
128
|
+
</View>
|
|
120
129
|
</View>
|
|
121
|
-
</
|
|
130
|
+
</CardComponent>
|
|
122
131
|
</TouchableOpacity>
|
|
123
132
|
);
|
|
124
133
|
});
|
|
125
134
|
|
|
135
|
+
|
|
126
136
|
SubscriptionPlanCard.displayName = "SubscriptionPlanCard";
|
|
127
137
|
|
|
128
138
|
const styles = StyleSheet.create({
|
|
129
139
|
container: {
|
|
130
140
|
borderRadius: 16,
|
|
131
|
-
padding: 18,
|
|
132
141
|
position: "relative",
|
|
142
|
+
overflow: "hidden", // Important for gradient borders/corners
|
|
143
|
+
},
|
|
144
|
+
gradientWrapper: {
|
|
145
|
+
flex: 1,
|
|
146
|
+
padding: 18,
|
|
133
147
|
},
|
|
134
148
|
content: {
|
|
135
149
|
flexDirection: "row",
|