@umituz/react-native-subscription 2.2.44 → 2.2.46
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 +12 -10
- package/src/presentation/components/feedback/PaywallFeedbackModal.tsx +18 -24
- package/src/presentation/components/paywall/CreditsTabContent.tsx +5 -5
- package/src/presentation/components/paywall/PaywallModal.tsx +6 -6
- package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +2 -2
- package/src/presentation/components/paywall/SubscriptionTabContent.tsx +8 -6
- package/src/revenuecat/infrastructure/utils/ApiKeyResolver.ts +35 -4
- package/src/revenuecat/infrastructure/utils/ExpoGoDetector.ts +31 -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.46",
|
|
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",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"@umituz/react-native-design-system": "latest",
|
|
32
32
|
"@umituz/react-native-firestore": "*",
|
|
33
33
|
"@umituz/react-native-legal": "*",
|
|
34
|
+
"@umituz/react-native-localization": ">=3.0.0",
|
|
34
35
|
"expo-constants": ">=18.0.0",
|
|
35
36
|
"firebase": ">=10.0.0",
|
|
36
37
|
"react": ">=18.2.0",
|
|
@@ -39,17 +40,18 @@
|
|
|
39
40
|
"react-native-safe-area-context": ">=4.0.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
|
-
"@tanstack/react-query": "^5.
|
|
43
|
+
"@tanstack/react-query": "^5.90.12",
|
|
43
44
|
"@types/node": "^25.0.3",
|
|
44
|
-
"@types/react": "~19.1.
|
|
45
|
+
"@types/react": "~19.1.10",
|
|
46
|
+
"@umituz/react-native-design-system": "latest",
|
|
45
47
|
"@umituz/react-native-firestore": "latest",
|
|
46
|
-
"@umituz/react-native-legal": "
|
|
47
|
-
"@umituz/react-native-localization": "
|
|
48
|
-
"expo-constants": "~18.0.
|
|
49
|
-
"firebase": "^
|
|
50
|
-
"react-native": "
|
|
51
|
-
"react-native-purchases": "
|
|
52
|
-
"react-native-safe-area-context": "
|
|
48
|
+
"@umituz/react-native-legal": "latest",
|
|
49
|
+
"@umituz/react-native-localization": "latest",
|
|
50
|
+
"expo-constants": "~18.0.12",
|
|
51
|
+
"firebase": "^12.6.0",
|
|
52
|
+
"react-native": "0.81.5",
|
|
53
|
+
"react-native-purchases": "^9.6.10",
|
|
54
|
+
"react-native-safe-area-context": "~5.6.0",
|
|
53
55
|
"typescript": "~5.9.2"
|
|
54
56
|
},
|
|
55
57
|
"publishConfig": {
|
|
@@ -19,13 +19,13 @@ import { useLocalization } from "@umituz/react-native-localization";
|
|
|
19
19
|
import { usePaywallFeedback } from "../../hooks/feedback/usePaywallFeedback";
|
|
20
20
|
import { createPaywallFeedbackStyles } from "./paywallFeedbackStyles";
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
];
|
|
22
|
+
const FEEDBACK_OPTION_IDS = [
|
|
23
|
+
"too_expensive",
|
|
24
|
+
"no_need",
|
|
25
|
+
"trying_out",
|
|
26
|
+
"technical_issues",
|
|
27
|
+
"other",
|
|
28
|
+
] as const;
|
|
29
29
|
|
|
30
30
|
export interface PaywallFeedbackModalProps {
|
|
31
31
|
visible: boolean;
|
|
@@ -64,14 +64,10 @@ export const PaywallFeedbackModal: React.FC<PaywallFeedbackModalProps> = React.m
|
|
|
64
64
|
[tokens, canSubmit],
|
|
65
65
|
);
|
|
66
66
|
|
|
67
|
-
const displayTitle = title || t("paywall.feedback.title"
|
|
68
|
-
const displaySubtitle = subtitle || t("paywall.feedback.subtitle"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const displaySubmitText = submitText || t("paywall.feedback.submit", { defaultValue: "Submit" });
|
|
72
|
-
const displayOtherPlaceholder = otherPlaceholder || t("paywall.feedback.otherPlaceholder", {
|
|
73
|
-
defaultValue: "Tell us more..."
|
|
74
|
-
});
|
|
67
|
+
const displayTitle = title || t("paywall.feedback.title");
|
|
68
|
+
const displaySubtitle = subtitle || t("paywall.feedback.subtitle");
|
|
69
|
+
const displaySubmitText = submitText || t("paywall.feedback.submit");
|
|
70
|
+
const displayOtherPlaceholder = otherPlaceholder || t("paywall.feedback.otherPlaceholder");
|
|
75
71
|
|
|
76
72
|
return (
|
|
77
73
|
<Modal
|
|
@@ -98,21 +94,19 @@ export const PaywallFeedbackModal: React.FC<PaywallFeedbackModalProps> = React.m
|
|
|
98
94
|
</View>
|
|
99
95
|
|
|
100
96
|
<View style={styles.optionsContainer}>
|
|
101
|
-
{
|
|
102
|
-
const isSelected = selectedReason ===
|
|
103
|
-
const isLast = index ===
|
|
104
|
-
const displayText = t(`paywall.feedback.reasons.${
|
|
105
|
-
defaultValue: option.defaultText
|
|
106
|
-
});
|
|
97
|
+
{FEEDBACK_OPTION_IDS.map((optionId, index) => {
|
|
98
|
+
const isSelected = selectedReason === optionId;
|
|
99
|
+
const isLast = index === FEEDBACK_OPTION_IDS.length - 1;
|
|
100
|
+
const displayText = t(`paywall.feedback.reasons.${optionId}`);
|
|
107
101
|
|
|
108
102
|
return (
|
|
109
|
-
<View key={
|
|
103
|
+
<View key={optionId}>
|
|
110
104
|
<TouchableOpacity
|
|
111
105
|
style={[
|
|
112
106
|
styles.optionRow,
|
|
113
107
|
isLast && styles.optionRowLast,
|
|
114
108
|
]}
|
|
115
|
-
onPress={() => selectReason(
|
|
109
|
+
onPress={() => selectReason(optionId)}
|
|
116
110
|
activeOpacity={0.7}
|
|
117
111
|
>
|
|
118
112
|
<AtomicText
|
|
@@ -137,7 +131,7 @@ export const PaywallFeedbackModal: React.FC<PaywallFeedbackModalProps> = React.m
|
|
|
137
131
|
</View>
|
|
138
132
|
</TouchableOpacity>
|
|
139
133
|
|
|
140
|
-
{isSelected &&
|
|
134
|
+
{isSelected && optionId === "other" && (
|
|
141
135
|
<View style={styles.inputContainer}>
|
|
142
136
|
<TextInput
|
|
143
137
|
style={styles.textInput}
|
|
@@ -44,11 +44,11 @@ export const CreditsTabContent: React.FC<CreditsTabContentProps> = React.memo(
|
|
|
44
44
|
const needsCredits = requiredCredits && requiredCredits > currentCredits;
|
|
45
45
|
|
|
46
46
|
const displayPurchaseButtonText = purchaseButtonText ||
|
|
47
|
-
t("paywall.purchase"
|
|
47
|
+
t("paywall.purchase");
|
|
48
48
|
const displayProcessingText = processingText ||
|
|
49
|
-
t("paywall.processing"
|
|
49
|
+
t("paywall.processing");
|
|
50
50
|
const displayCreditsInfoText = creditsInfoText ||
|
|
51
|
-
t("paywall.creditsInfo"
|
|
51
|
+
t("paywall.creditsInfo");
|
|
52
52
|
|
|
53
53
|
return (
|
|
54
54
|
<View style={styles.container}>
|
|
@@ -64,8 +64,8 @@ export const CreditsTabContent: React.FC<CreditsTabContentProps> = React.memo(
|
|
|
64
64
|
style={{ color: tokens.colors.textSecondary }}
|
|
65
65
|
>
|
|
66
66
|
{displayCreditsInfoText
|
|
67
|
-
.replace("{required}", String(requiredCredits))
|
|
68
|
-
.replace("{current}", String(currentCredits))}
|
|
67
|
+
.replace("{{required}}", String(requiredCredits))
|
|
68
|
+
.replace("{{current}}", String(currentCredits))}
|
|
69
69
|
</AtomicText>
|
|
70
70
|
</View>
|
|
71
71
|
)}
|
|
@@ -110,8 +110,8 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo(
|
|
|
110
110
|
onSubscriptionPurchase,
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
const displayTitle = title || t("paywall.title"
|
|
114
|
-
const displaySubtitle = subtitle || t("paywall.subtitle"
|
|
113
|
+
const displayTitle = title || t("paywall.title");
|
|
114
|
+
const displaySubtitle = subtitle || t("paywall.subtitle");
|
|
115
115
|
|
|
116
116
|
useEffect(() => {
|
|
117
117
|
if (__DEV__) {
|
|
@@ -161,8 +161,8 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo(
|
|
|
161
161
|
<PaywallTabBar
|
|
162
162
|
activeTab={activeTab}
|
|
163
163
|
onTabChange={handleTabChange}
|
|
164
|
-
creditsLabel={t("paywall.tabs.credits"
|
|
165
|
-
subscriptionLabel={t("paywall.tabs.subscription"
|
|
164
|
+
creditsLabel={t("paywall.tabs.credits")}
|
|
165
|
+
subscriptionLabel={t("paywall.tabs.subscription")}
|
|
166
166
|
/>
|
|
167
167
|
|
|
168
168
|
<View style={{ flex: 1 }}>
|
|
@@ -175,7 +175,7 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo(
|
|
|
175
175
|
currentCredits={currentCredits}
|
|
176
176
|
requiredCredits={requiredCredits}
|
|
177
177
|
isLoading={isLoading}
|
|
178
|
-
purchaseButtonText={t("paywall.purchase"
|
|
178
|
+
purchaseButtonText={t("paywall.purchase")}
|
|
179
179
|
/>
|
|
180
180
|
) : (
|
|
181
181
|
<SubscriptionTabContent
|
|
@@ -185,7 +185,7 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo(
|
|
|
185
185
|
onPurchase={handleSubscriptionPurchase}
|
|
186
186
|
features={subscriptionFeatures}
|
|
187
187
|
isLoading={isLoading}
|
|
188
|
-
purchaseButtonText={t("paywall.subscribe"
|
|
188
|
+
purchaseButtonText={t("paywall.subscribe")}
|
|
189
189
|
onRestore={onRestore}
|
|
190
190
|
privacyUrl={privacyUrl}
|
|
191
191
|
termsUrl={termsUrl}
|
|
@@ -44,7 +44,7 @@ export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
|
44
44
|
? formatPrice(pkg.product.price / 12, pkg.product.currencyCode)
|
|
45
45
|
: null;
|
|
46
46
|
|
|
47
|
-
const title = pkg.product.title || t(`paywall.${periodLabel}
|
|
47
|
+
const title = pkg.product.title || t(`paywall.period.${periodLabel}`);
|
|
48
48
|
|
|
49
49
|
return (
|
|
50
50
|
<TouchableOpacity
|
|
@@ -71,7 +71,7 @@ export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
|
71
71
|
type="labelSmall"
|
|
72
72
|
style={{ color: tokens.colors.onPrimary, fontWeight: "600" }}
|
|
73
73
|
>
|
|
74
|
-
{t("paywall.bestValue"
|
|
74
|
+
{t("paywall.bestValue")}
|
|
75
75
|
</AtomicText>
|
|
76
76
|
</View>
|
|
77
77
|
)}
|
|
@@ -56,7 +56,7 @@ export const SubscriptionTabContent: React.FC<SubscriptionTabContentProps> =
|
|
|
56
56
|
isLoading = false,
|
|
57
57
|
purchaseButtonText,
|
|
58
58
|
processingText,
|
|
59
|
-
restoreButtonText
|
|
59
|
+
restoreButtonText,
|
|
60
60
|
privacyUrl,
|
|
61
61
|
termsUrl,
|
|
62
62
|
privacyText,
|
|
@@ -66,9 +66,11 @@ export const SubscriptionTabContent: React.FC<SubscriptionTabContentProps> =
|
|
|
66
66
|
const { t } = useLocalization();
|
|
67
67
|
|
|
68
68
|
const displayPurchaseButtonText =
|
|
69
|
-
purchaseButtonText || t("paywall.subscribe"
|
|
69
|
+
purchaseButtonText || t("paywall.subscribe");
|
|
70
70
|
const displayProcessingText =
|
|
71
|
-
processingText || t("paywall.processing"
|
|
71
|
+
processingText || t("paywall.processing");
|
|
72
|
+
const displayRestoreButtonText =
|
|
73
|
+
restoreButtonText || t("paywall.restore");
|
|
72
74
|
|
|
73
75
|
const sortedPackages = useMemo(() => sortPackages(packages), [packages]);
|
|
74
76
|
|
|
@@ -84,8 +86,8 @@ export const SubscriptionTabContent: React.FC<SubscriptionTabContentProps> =
|
|
|
84
86
|
isLoading={isLoading}
|
|
85
87
|
selectedPkg={selectedPackage}
|
|
86
88
|
onSelect={onSelectPackage}
|
|
87
|
-
loadingText={t("paywall.loading"
|
|
88
|
-
emptyText={t("paywall.empty"
|
|
89
|
+
loadingText={t("paywall.loading")}
|
|
90
|
+
emptyText={t("paywall.empty")}
|
|
89
91
|
/>
|
|
90
92
|
|
|
91
93
|
{features.length > 0 && (
|
|
@@ -107,7 +109,7 @@ export const SubscriptionTabContent: React.FC<SubscriptionTabContentProps> =
|
|
|
107
109
|
purchaseButtonText={displayPurchaseButtonText}
|
|
108
110
|
hasPackages={packages.length > 0}
|
|
109
111
|
selectedPkg={selectedPackage}
|
|
110
|
-
restoreButtonText={
|
|
112
|
+
restoreButtonText={displayRestoreButtonText}
|
|
111
113
|
showRestoreButton={!!onRestore}
|
|
112
114
|
onPurchase={onPurchase}
|
|
113
115
|
onRestore={onRestore || (() => { })}
|
|
@@ -5,22 +5,50 @@
|
|
|
5
5
|
|
|
6
6
|
import { Platform } from "react-native";
|
|
7
7
|
import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
|
|
8
|
-
import { isExpoGo,
|
|
8
|
+
import { isExpoGo, isProductionBuild } from "./ExpoGoDetector";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Check if Test Store key should be used
|
|
12
|
+
* CRITICAL: Never use test store in production builds
|
|
12
13
|
*/
|
|
13
14
|
export function shouldUseTestStore(config: RevenueCatConfig): boolean {
|
|
14
15
|
const testKey = config.testStoreKey;
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
// No test key configured - always use production keys
|
|
18
|
+
if (!testKey) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// CRITICAL: Production builds should NEVER use test store
|
|
23
|
+
if (isProductionBuild() && !isExpoGo()) {
|
|
24
|
+
if (__DEV__) {
|
|
25
|
+
console.log("[RevenueCat] Production build detected - using production API keys");
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Only use test store in Expo Go
|
|
31
|
+
return isExpoGo();
|
|
16
32
|
}
|
|
17
33
|
|
|
18
34
|
/**
|
|
19
35
|
* Get RevenueCat API key from config
|
|
20
|
-
* Returns Test Store key if in
|
|
36
|
+
* Returns Test Store key if in Expo Go environment ONLY
|
|
21
37
|
*/
|
|
22
38
|
export function resolveApiKey(config: RevenueCatConfig): string | null {
|
|
23
|
-
|
|
39
|
+
const useTestStore = shouldUseTestStore(config);
|
|
40
|
+
|
|
41
|
+
if (__DEV__) {
|
|
42
|
+
console.log("[RevenueCat] resolveApiKey:", {
|
|
43
|
+
platform: Platform.OS,
|
|
44
|
+
useTestStore,
|
|
45
|
+
hasTestKey: !!config.testStoreKey,
|
|
46
|
+
hasIosKey: !!config.iosApiKey,
|
|
47
|
+
hasAndroidKey: !!config.androidApiKey,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (useTestStore) {
|
|
24
52
|
return config.testStoreKey ?? null;
|
|
25
53
|
}
|
|
26
54
|
|
|
@@ -31,6 +59,9 @@ export function resolveApiKey(config: RevenueCatConfig): string | null {
|
|
|
31
59
|
: config.iosApiKey;
|
|
32
60
|
|
|
33
61
|
if (!key || key === "" || key.includes("YOUR_")) {
|
|
62
|
+
if (__DEV__) {
|
|
63
|
+
console.warn("[RevenueCat] No valid API key found for platform:", Platform.OS);
|
|
64
|
+
}
|
|
34
65
|
return null;
|
|
35
66
|
}
|
|
36
67
|
|
|
@@ -14,14 +14,45 @@ export function isExpoGo(): boolean {
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Check if running in development mode
|
|
17
|
+
* Uses multiple checks to ensure reliability in production builds
|
|
17
18
|
*/
|
|
18
19
|
export function isDevelopment(): boolean {
|
|
20
|
+
// Check execution environment first - most reliable
|
|
21
|
+
const executionEnv = Constants.executionEnvironment;
|
|
22
|
+
const isBareBuild = executionEnv === "bare";
|
|
23
|
+
const isStoreBuild = executionEnv === "standalone";
|
|
24
|
+
|
|
25
|
+
// If it's a store/standalone build, it's NOT development
|
|
26
|
+
if (isStoreBuild) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// For bare builds in production, check appOwnership
|
|
31
|
+
if (isBareBuild && Constants.appOwnership !== "expo") {
|
|
32
|
+
// This is a production bare build
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fallback to __DEV__ only for actual development cases
|
|
19
37
|
return typeof __DEV__ !== "undefined" && __DEV__;
|
|
20
38
|
}
|
|
21
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Check if this is a production store build
|
|
42
|
+
*/
|
|
43
|
+
export function isProductionBuild(): boolean {
|
|
44
|
+
const executionEnv = Constants.executionEnvironment;
|
|
45
|
+
return executionEnv === "standalone" || executionEnv === "bare";
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
/**
|
|
23
49
|
* Check if Test Store should be used (Expo Go or development)
|
|
50
|
+
* NEVER use Test Store in production builds
|
|
24
51
|
*/
|
|
25
52
|
export function isTestStoreEnvironment(): boolean {
|
|
53
|
+
// Explicit check: never use test store in production
|
|
54
|
+
if (isProductionBuild() && !isExpoGo()) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
26
57
|
return isExpoGo() || isDevelopment();
|
|
27
58
|
}
|