@umituz/react-native-subscription 2.10.7 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.10.7",
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 { View, StyleSheet } from "react-native";
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
- <View
23
- style={[styles.badge, { backgroundColor: tokens.colors.primary }]}
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: "700" }}
27
+ style={{ color: tokens.colors.onPrimary, fontWeight: "800", textTransform: "uppercase", fontSize: 10 }}
28
28
  >
29
29
  {text}
30
30
  </AtomicText>
31
- </View>
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 style={[styles.legalLinksContainer, { borderColor: tokens.colors.border }]}>
67
- {privacyUrl && (
68
- <TouchableOpacity
69
- onPress={handlePrivacyPress}
70
- activeOpacity={0.6}
71
- style={styles.linkItem}
72
- >
73
- <AtomicText
74
- type="labelSmall"
75
- style={[styles.linkText, { color: tokens.colors.textSecondary }]}
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
- {privacyText}
78
- </AtomicText>
79
- </TouchableOpacity>
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 && termsUrl && (
83
- <View style={[styles.dot, { backgroundColor: tokens.colors.border }]} />
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={[styles.linkText, { color: tokens.colors.textSecondary }]}
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 { AtomicButton, AtomicText } from "@umituz/react-native-design-system";
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
- style={styles.restoreButton}
56
- onPress={onRestore}
57
- disabled={isProcessing || isLoading}
53
+ onPress={onPurchase}
54
+ disabled={isDisabled}
55
+ activeOpacity={0.8}
58
56
  >
59
- <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
60
- {restoreButtonText}
61
- </AtomicText>
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.border,
71
+ : tokens.colors.borderLight,
63
72
  borderWidth: isSelected ? 2 : 1,
73
+ backgroundColor: isSelected ? undefined : tokens.colors.surface,
64
74
  },
65
75
  ]}
66
76
  >
67
- <BestValueBadge
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
- <View style={styles.content}>
73
- <View style={styles.leftSection}>
74
- <View
75
- style={[
76
- styles.radio,
77
- {
78
- borderColor: isSelected
79
- ? tokens.colors.primary
80
- : tokens.colors.border,
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
- {title}
99
- </AtomicText>
100
- {isYearly && (
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="bodySmall"
103
- style={{ color: tokens.colors.textSecondary }}
103
+ type="titleSmall"
104
+ style={[styles.title, { color: tokens.colors.textPrimary }]}
104
105
  >
105
- {price}
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
- <View style={styles.rightSection}>
112
- <AtomicText
113
- type="titleMedium"
114
- style={[styles.price, { color: tokens.colors.textPrimary }]}
115
- >
116
- {isYearly && monthlyEquivalent
117
- ? `${monthlyEquivalent}/mo`
118
- : price}
119
- </AtomicText>
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
- </View>
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",
@@ -128,11 +128,7 @@ export async function initializeSDK(
128
128
  console.log("[RevenueCat] Calling Purchases.configure()...");
129
129
  }
130
130
 
131
- if (deps.isUsingTestStore()) {
132
- await Purchases.configureInAppBrowserMode();
133
- } else {
134
- await Purchases.configure({ apiKey: key, appUserID: userId });
135
- }
131
+ await Purchases.configure({ apiKey: key, appUserID: userId });
136
132
  deps.setInitialized(true);
137
133
  deps.setCurrentUserId(userId);
138
134