@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.2.44",
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.0.0",
43
+ "@tanstack/react-query": "^5.90.12",
43
44
  "@types/node": "^25.0.3",
44
- "@types/react": "~19.1.0",
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": "^2.1.2",
47
- "@umituz/react-native-localization": "^3.5.8",
48
- "expo-constants": "~18.0.0",
49
- "firebase": "^11.0.0",
50
- "react-native": "~0.76.0",
51
- "react-native-purchases": "~9.6.0",
52
- "react-native-safe-area-context": "^5.6.2",
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 FEEDBACK_OPTIONS = [
23
- { id: "too_expensive", defaultText: "Too expensive" },
24
- { id: "no_need", defaultText: "I don't need premium features" },
25
- { id: "trying_out", defaultText: "Just trying it out" },
26
- { id: "technical_issues", defaultText: "Technical issues" },
27
- { id: "other", defaultText: "Other" },
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", { defaultValue: "Help us improve" });
68
- const displaySubtitle = subtitle || t("paywall.feedback.subtitle", {
69
- defaultValue: "We'd love to know why you decided not to join Premium today."
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
- {FEEDBACK_OPTIONS.map((option, index) => {
102
- const isSelected = selectedReason === option.id;
103
- const isLast = index === FEEDBACK_OPTIONS.length - 1;
104
- const displayText = t(`paywall.feedback.reasons.${option.id}`, {
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={option.id}>
103
+ <View key={optionId}>
110
104
  <TouchableOpacity
111
105
  style={[
112
106
  styles.optionRow,
113
107
  isLast && styles.optionRowLast,
114
108
  ]}
115
- onPress={() => selectReason(option.id)}
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 && option.id === "other" && (
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", { defaultValue: "Purchase" });
47
+ t("paywall.purchase");
48
48
  const displayProcessingText = processingText ||
49
- t("paywall.processing", { defaultValue: "Processing..." });
49
+ t("paywall.processing");
50
50
  const displayCreditsInfoText = creditsInfoText ||
51
- t("paywall.creditsInfo", { defaultValue: "You need {required} credits. You have {current}." });
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", { defaultValue: "Get Premium" });
114
- const displaySubtitle = subtitle || t("paywall.subtitle", { defaultValue: "" });
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", { defaultValue: "Credits" })}
165
- subscriptionLabel={t("paywall.tabs.subscription", { defaultValue: "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", { defaultValue: "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", { defaultValue: "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}`, { defaultValue: 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", { defaultValue: "Best Value" })}
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 = "Restore Purchases",
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", { defaultValue: "Subscribe" });
69
+ purchaseButtonText || t("paywall.subscribe");
70
70
  const displayProcessingText =
71
- processingText || t("paywall.processing", { defaultValue: "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", { defaultValue: "Loading..." })}
88
- emptyText={t("paywall.empty", { defaultValue: "No packages" })}
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={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, isDevelopment } from "./ExpoGoDetector";
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
- return !!(testKey && (isExpoGo() || isDevelopment()));
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 dev/Expo Go environment
36
+ * Returns Test Store key if in Expo Go environment ONLY
21
37
  */
22
38
  export function resolveApiKey(config: RevenueCatConfig): string | null {
23
- if (shouldUseTestStore(config)) {
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
  }