@umituz/react-native-subscription 2.27.97 → 2.27.98
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 +1 -1
- package/src/domains/credits/presentation/useDeductCredit.ts +0 -4
- package/src/domains/paywall/components/PaywallModal.tsx +0 -6
- package/src/domains/subscription/application/SubscriptionInitializer.ts +0 -6
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +0 -9
- package/src/domains/subscription/infrastructure/hooks/useCustomerInfo.ts +0 -19
- package/src/domains/subscription/infrastructure/hooks/useInitializeSubscription.ts +0 -16
- package/src/domains/subscription/infrastructure/hooks/usePurchasePackage.ts +0 -31
- package/src/domains/subscription/infrastructure/hooks/useRestorePurchase.ts +0 -27
- package/src/domains/subscription/infrastructure/hooks/useRevenueCatTrialEligibility.ts +0 -12
- package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +0 -21
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +0 -9
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +0 -15
- package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.tsx +2 -16
- package/src/domains/subscription/presentation/stores/purchaseLoadingStore.ts +0 -3
- package/src/domains/subscription/presentation/useFeatureGate.ts +0 -50
- package/src/domains/subscription/presentation/screens/components/DevTestSection.tsx +0 -133
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.27.
|
|
3
|
+
"version": "2.27.98",
|
|
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",
|
|
@@ -82,24 +82,20 @@ export const useDeductCredit = ({
|
|
|
82
82
|
|
|
83
83
|
const deductCredit = useCallback(async (cost: number = 1): Promise<boolean> => {
|
|
84
84
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
85
|
-
console.log("[useDeductCredit] Attempting to deduct:", cost);
|
|
86
85
|
}
|
|
87
86
|
try {
|
|
88
87
|
const res = await mutation.mutateAsync(cost);
|
|
89
88
|
if (!res.success) {
|
|
90
89
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
91
|
-
console.log("[useDeductCredit] Deduction failed:", res.error?.code, res.error?.message);
|
|
92
90
|
}
|
|
93
91
|
if (res.error?.code === "CREDITS_EXHAUSTED") onCreditsExhausted?.();
|
|
94
92
|
return false;
|
|
95
93
|
}
|
|
96
94
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
97
|
-
console.log("[useDeductCredit] Deduction successful, remaining:", res.remainingCredits);
|
|
98
95
|
}
|
|
99
96
|
return true;
|
|
100
97
|
} catch (err) {
|
|
101
98
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
102
|
-
console.log("[useDeductCredit] Deduction error:", err);
|
|
103
99
|
}
|
|
104
100
|
return false;
|
|
105
101
|
}
|
|
@@ -74,7 +74,6 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
74
74
|
if (!selectedPlanId || !onPurchase) return;
|
|
75
75
|
|
|
76
76
|
if (__DEV__) {
|
|
77
|
-
console.log("[PaywallModal] handlePurchase starting:", { selectedPlanId });
|
|
78
77
|
}
|
|
79
78
|
|
|
80
79
|
setIsLocalProcessing(true);
|
|
@@ -84,11 +83,9 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
84
83
|
const pkg = packages.find((p) => p.product.identifier === selectedPlanId);
|
|
85
84
|
if (pkg) {
|
|
86
85
|
if (__DEV__) {
|
|
87
|
-
console.log("[PaywallModal] Calling onPurchase:", { productId: pkg.product.identifier });
|
|
88
86
|
}
|
|
89
87
|
await onPurchase(pkg);
|
|
90
88
|
if (__DEV__) {
|
|
91
|
-
console.log("[PaywallModal] onPurchase completed");
|
|
92
89
|
}
|
|
93
90
|
}
|
|
94
91
|
} catch (error) {
|
|
@@ -100,7 +97,6 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
100
97
|
setIsLocalProcessing(false);
|
|
101
98
|
endPurchase();
|
|
102
99
|
if (__DEV__) {
|
|
103
|
-
console.log("[PaywallModal] handlePurchase finished");
|
|
104
100
|
}
|
|
105
101
|
}
|
|
106
102
|
}, [selectedPlanId, packages, onPurchase, startPurchase, endPurchase]);
|
|
@@ -109,14 +105,12 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
109
105
|
if (!onRestore || isProcessing) return;
|
|
110
106
|
|
|
111
107
|
if (__DEV__) {
|
|
112
|
-
console.log("[PaywallModal] handleRestore starting");
|
|
113
108
|
}
|
|
114
109
|
|
|
115
110
|
setIsLocalProcessing(true);
|
|
116
111
|
try {
|
|
117
112
|
await onRestore();
|
|
118
113
|
if (__DEV__) {
|
|
119
|
-
console.log("[PaywallModal] handleRestore completed");
|
|
120
114
|
}
|
|
121
115
|
} finally {
|
|
122
116
|
setIsLocalProcessing(false);
|
|
@@ -94,9 +94,6 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
|
|
|
94
94
|
|
|
95
95
|
const initializeInBackground = async (userId?: string): Promise<void> => {
|
|
96
96
|
await SubscriptionManager.initialize(userId);
|
|
97
|
-
if (__DEV__) {
|
|
98
|
-
console.log('[SubscriptionInitializer] Background init complete');
|
|
99
|
-
}
|
|
100
97
|
};
|
|
101
98
|
|
|
102
99
|
// 5. Start Background Init
|
|
@@ -110,9 +107,6 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
|
|
|
110
107
|
|
|
111
108
|
// 6. Listen for Auth Changes
|
|
112
109
|
setupAuthStateListener(() => auth, (newUserId) => {
|
|
113
|
-
if (__DEV__) {
|
|
114
|
-
console.log('[SubscriptionInitializer] Auth changed, re-init:', newUserId);
|
|
115
|
-
}
|
|
116
110
|
initializeInBackground(newUserId);
|
|
117
111
|
});
|
|
118
112
|
};
|
|
@@ -32,27 +32,18 @@ export class PackageHandler {
|
|
|
32
32
|
const offering = await this.service.fetchOfferings();
|
|
33
33
|
|
|
34
34
|
if (!offering) {
|
|
35
|
-
if (__DEV__) {
|
|
36
|
-
console.warn("[PackageHandler] No offerings available from RevenueCat");
|
|
37
|
-
}
|
|
38
35
|
// Return empty array instead of throwing - allows graceful degradation
|
|
39
36
|
return [];
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
const packages = offering.availablePackages;
|
|
43
40
|
if (!packages || packages.length === 0) {
|
|
44
|
-
if (__DEV__) {
|
|
45
|
-
console.warn("[PackageHandler] No packages available in offering");
|
|
46
|
-
}
|
|
47
41
|
// Return empty array instead of throwing - allows graceful degradation
|
|
48
42
|
return [];
|
|
49
43
|
}
|
|
50
44
|
|
|
51
45
|
return packages;
|
|
52
46
|
} catch (error) {
|
|
53
|
-
if (__DEV__) {
|
|
54
|
-
console.error("[PackageHandler] Failed to fetch packages:", error);
|
|
55
|
-
}
|
|
56
47
|
// Re-throw with more context
|
|
57
48
|
throw new Error(
|
|
58
49
|
`Failed to fetch subscription packages. ${
|
|
@@ -71,23 +71,10 @@ export function useCustomerInfo(): UseCustomerInfoResult {
|
|
|
71
71
|
const info = await Purchases.getCustomerInfo();
|
|
72
72
|
|
|
73
73
|
setCustomerInfo(info);
|
|
74
|
-
|
|
75
|
-
if (__DEV__) {
|
|
76
|
-
console.log('[DEBUG useCustomerInfo] Fetched:', {
|
|
77
|
-
hasActiveEntitlements: Object.keys(info.entitlements.active).length > 0,
|
|
78
|
-
latestExpiration: info.latestExpirationDate || "none",
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
74
|
} catch (err) {
|
|
82
75
|
const errorMessage =
|
|
83
76
|
err instanceof Error ? err.message : "Failed to fetch customer info";
|
|
84
77
|
setError(errorMessage);
|
|
85
|
-
|
|
86
|
-
if (__DEV__) {
|
|
87
|
-
console.error('[DEBUG useCustomerInfo] Error:', {
|
|
88
|
-
error: err,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
78
|
} finally {
|
|
92
79
|
setLoading(false);
|
|
93
80
|
setIsFetching(false);
|
|
@@ -100,12 +87,6 @@ export function useCustomerInfo(): UseCustomerInfoResult {
|
|
|
100
87
|
|
|
101
88
|
// Listen for real-time updates (renewals, purchases, restore)
|
|
102
89
|
const listener = (info: CustomerInfo) => {
|
|
103
|
-
if (__DEV__) {
|
|
104
|
-
console.log('[DEBUG useCustomerInfo] Listener update:', {
|
|
105
|
-
hasActiveEntitlements: Object.keys(info.entitlements.active).length > 0,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
90
|
setCustomerInfo(info);
|
|
110
91
|
setError(null);
|
|
111
92
|
};
|
|
@@ -19,10 +19,6 @@ export const useInitializeSubscription = (userId: string | undefined) => {
|
|
|
19
19
|
throw new Error("User not authenticated");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
if (__DEV__) {
|
|
23
|
-
console.log('[DEBUG useInitializeSubscription] Initializing:', { userId });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
22
|
return SubscriptionManager.initialize(userId);
|
|
27
23
|
},
|
|
28
24
|
onSuccess: () => {
|
|
@@ -30,18 +26,6 @@ export const useInitializeSubscription = (userId: string | undefined) => {
|
|
|
30
26
|
queryClient.invalidateQueries({
|
|
31
27
|
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
32
28
|
});
|
|
33
|
-
|
|
34
|
-
if (__DEV__) {
|
|
35
|
-
console.log('[DEBUG useInitializeSubscription] Success:', { userId });
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
onError: (error) => {
|
|
40
|
-
if (__DEV__) {
|
|
41
|
-
console.error('[DEBUG useInitializeSubscription] Error:', {
|
|
42
|
-
error,
|
|
43
|
-
userId: userId ?? "ANONYMOUS",
|
|
44
|
-
});
|
|
45
29
|
}
|
|
46
30
|
},
|
|
47
31
|
});
|
|
@@ -40,33 +40,12 @@ export const usePurchasePackage = () => {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const productId = pkg.product.identifier;
|
|
43
|
-
|
|
44
|
-
if (__DEV__) {
|
|
45
|
-
console.log('[DEBUG usePurchasePackage] Starting purchase:', {
|
|
46
|
-
packageId: pkg.identifier,
|
|
47
|
-
productId,
|
|
48
|
-
userId,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
43
|
const success = await SubscriptionManager.purchasePackage(pkg);
|
|
53
44
|
|
|
54
|
-
if (__DEV__) {
|
|
55
|
-
console.log('[DEBUG usePurchasePackage] Purchase result:', {
|
|
56
|
-
success,
|
|
57
|
-
packageId: pkg.identifier,
|
|
58
|
-
productId,
|
|
59
|
-
userId,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
45
|
return { success, productId };
|
|
64
46
|
},
|
|
65
47
|
onSuccess: (result) => {
|
|
66
48
|
if (result.success) {
|
|
67
|
-
if (__DEV__) {
|
|
68
|
-
console.log('[DEBUG usePurchasePackage] onSuccess - invalidating queries');
|
|
69
|
-
}
|
|
70
49
|
showSuccess("Purchase Successful", "Your subscription is now active!");
|
|
71
50
|
queryClient.invalidateQueries({
|
|
72
51
|
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
@@ -77,20 +56,10 @@ export const usePurchasePackage = () => {
|
|
|
77
56
|
});
|
|
78
57
|
}
|
|
79
58
|
} else {
|
|
80
|
-
if (__DEV__) {
|
|
81
|
-
console.log('[DEBUG usePurchasePackage] onSuccess but result.success=false');
|
|
82
|
-
}
|
|
83
59
|
showError("Purchase Failed", "Unable to complete purchase. Please try again.");
|
|
84
60
|
}
|
|
85
61
|
},
|
|
86
62
|
onError: (error) => {
|
|
87
|
-
if (__DEV__) {
|
|
88
|
-
console.error('[DEBUG usePurchasePackage] onError:', {
|
|
89
|
-
error,
|
|
90
|
-
userId: userId ?? "ANONYMOUS",
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
63
|
let title = "Purchase Error";
|
|
95
64
|
let message = "Unable to complete purchase. Please try again.";
|
|
96
65
|
|
|
@@ -34,26 +34,7 @@ export const useRestorePurchase = () => {
|
|
|
34
34
|
throw new Error("User not authenticated");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
if (__DEV__) {
|
|
38
|
-
console.log('[DEBUG useRestorePurchase] Starting restore:', { userId });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
37
|
const result = await SubscriptionManager.restore();
|
|
42
|
-
|
|
43
|
-
if (result.success) {
|
|
44
|
-
if (__DEV__) {
|
|
45
|
-
console.log('[DEBUG useRestorePurchase] Restore successful:', {
|
|
46
|
-
userId,
|
|
47
|
-
productId: result.productId,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
// Credits will be initialized by CustomerInfoListener
|
|
51
|
-
} else {
|
|
52
|
-
if (__DEV__) {
|
|
53
|
-
console.log('[DEBUG useRestorePurchase] Restore failed:', { userId });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
38
|
return result;
|
|
58
39
|
},
|
|
59
40
|
onSuccess: (result) => {
|
|
@@ -68,13 +49,5 @@ export const useRestorePurchase = () => {
|
|
|
68
49
|
}
|
|
69
50
|
}
|
|
70
51
|
},
|
|
71
|
-
onError: (error) => {
|
|
72
|
-
if (__DEV__) {
|
|
73
|
-
console.error('[DEBUG useRestorePurchase] Restore mutation failed:', {
|
|
74
|
-
error,
|
|
75
|
-
userId: userId ?? "ANONYMOUS",
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
52
|
});
|
|
80
53
|
};
|
|
@@ -85,9 +85,6 @@ export function useRevenueCatTrialEligibility(): UseRevenueCatTrialEligibilityRe
|
|
|
85
85
|
|
|
86
86
|
const service = getRevenueCatService();
|
|
87
87
|
if (!service || !service.isInitialized()) {
|
|
88
|
-
if (__DEV__) {
|
|
89
|
-
console.log("[TrialEligibility] RevenueCat not initialized");
|
|
90
|
-
}
|
|
91
88
|
return;
|
|
92
89
|
}
|
|
93
90
|
|
|
@@ -109,12 +106,6 @@ export function useRevenueCatTrialEligibility(): UseRevenueCatTrialEligibilityRe
|
|
|
109
106
|
eligible: isEligible,
|
|
110
107
|
trialDurationDays: 7, // Default to 7 days as configured in App Store Connect
|
|
111
108
|
};
|
|
112
|
-
|
|
113
|
-
if (__DEV__) {
|
|
114
|
-
console.log(
|
|
115
|
-
`[TrialEligibility] ${productId}: ${isEligible ? "ELIGIBLE" : "NOT_ELIGIBLE"}`
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
109
|
}
|
|
119
110
|
|
|
120
111
|
// Update cache
|
|
@@ -127,9 +118,6 @@ export function useRevenueCatTrialEligibility(): UseRevenueCatTrialEligibilityRe
|
|
|
127
118
|
setEligibilityMap((prev) => ({ ...prev, ...newMap }));
|
|
128
119
|
}
|
|
129
120
|
} catch (error) {
|
|
130
|
-
if (__DEV__) {
|
|
131
|
-
console.log("[TrialEligibility] Error checking eligibility:", error);
|
|
132
|
-
}
|
|
133
121
|
// On error, default to eligible (better UX)
|
|
134
122
|
const fallbackMap: TrialEligibilityMap = {};
|
|
135
123
|
for (const productId of productIds) {
|
|
@@ -58,14 +58,6 @@ export class CustomerInfoListenerManager {
|
|
|
58
58
|
|
|
59
59
|
// Handle renewal (same product, extended expiration)
|
|
60
60
|
if (renewalResult.isRenewal && config.onRenewalDetected) {
|
|
61
|
-
if (__DEV__) {
|
|
62
|
-
console.log("[CustomerInfoListener] Renewal detected:", {
|
|
63
|
-
userId: this.currentUserId,
|
|
64
|
-
productId: renewalResult.productId,
|
|
65
|
-
newExpiration: renewalResult.newExpirationDate,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
61
|
try {
|
|
70
62
|
await config.onRenewalDetected(
|
|
71
63
|
this.currentUserId,
|
|
@@ -82,16 +74,6 @@ export class CustomerInfoListenerManager {
|
|
|
82
74
|
|
|
83
75
|
// Handle plan change (upgrade/downgrade)
|
|
84
76
|
if (renewalResult.isPlanChange && config.onPlanChanged) {
|
|
85
|
-
if (__DEV__) {
|
|
86
|
-
console.log("[CustomerInfoListener] Plan change detected:", {
|
|
87
|
-
userId: this.currentUserId,
|
|
88
|
-
previousProductId: renewalResult.previousProductId,
|
|
89
|
-
newProductId: renewalResult.productId,
|
|
90
|
-
isUpgrade: renewalResult.isUpgrade,
|
|
91
|
-
isDowngrade: renewalResult.isDowngrade,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
77
|
try {
|
|
96
78
|
await config.onPlanChanged(
|
|
97
79
|
this.currentUserId,
|
|
@@ -133,9 +115,6 @@ export class CustomerInfoListenerManager {
|
|
|
133
115
|
}
|
|
134
116
|
|
|
135
117
|
destroy(): void {
|
|
136
|
-
if (__DEV__) {
|
|
137
|
-
console.log('[CustomerInfoListenerManager] Destroying listener manager');
|
|
138
|
-
}
|
|
139
118
|
this.removeListener();
|
|
140
119
|
this.clearUserId();
|
|
141
120
|
// Reset renewal state to ensure clean state
|
|
@@ -85,15 +85,12 @@ export async function initializeSDK(
|
|
|
85
85
|
|
|
86
86
|
const key = apiKey || resolveApiKey(deps.config);
|
|
87
87
|
if (!key) {
|
|
88
|
-
if (__DEV__) console.log('[RevenueCat] No API key');
|
|
89
88
|
return { success: false, offering: null, isPremium: false };
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
configurationState.configurationInProgress = true;
|
|
93
92
|
try {
|
|
94
93
|
configureLogHandler();
|
|
95
|
-
if (__DEV__) console.log('[RevenueCat] Configuring:', key.substring(0, 10) + '...');
|
|
96
|
-
|
|
97
94
|
await Purchases.configure({ apiKey: key, appUserID: userId });
|
|
98
95
|
configurationState.isPurchasesConfigured = true;
|
|
99
96
|
deps.setInitialized(true);
|
|
@@ -104,14 +101,8 @@ export async function initializeSDK(
|
|
|
104
101
|
Purchases.getOfferings(),
|
|
105
102
|
]);
|
|
106
103
|
|
|
107
|
-
if (__DEV__) {
|
|
108
|
-
console.log('[RevenueCat] Initialized', {
|
|
109
|
-
packages: offerings.current?.availablePackages?.length ?? 0,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
104
|
return buildSuccessResult(deps, customerInfo, offerings);
|
|
113
105
|
} catch (error) {
|
|
114
|
-
if (__DEV__) console.error('[RevenueCat] Init failed:', error);
|
|
115
106
|
return { success: false, offering: null, isPremium: false };
|
|
116
107
|
} finally {
|
|
117
108
|
configurationState.configurationInProgress = false;
|
|
@@ -49,27 +49,12 @@ export async function notifyPurchaseCompleted(
|
|
|
49
49
|
customerInfo: CustomerInfo,
|
|
50
50
|
source?: PurchaseSource
|
|
51
51
|
): Promise<void> {
|
|
52
|
-
if (__DEV__) {
|
|
53
|
-
console.log('[PremiumStatusSyncer] notifyPurchaseCompleted called:', {
|
|
54
|
-
userId,
|
|
55
|
-
productId,
|
|
56
|
-
source,
|
|
57
|
-
hasCallback: !!config.onPurchaseCompleted,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
52
|
if (!config.onPurchaseCompleted) {
|
|
62
|
-
if (__DEV__) {
|
|
63
|
-
console.warn('[PremiumStatusSyncer] No onPurchaseCompleted callback configured!');
|
|
64
|
-
}
|
|
65
53
|
return;
|
|
66
54
|
}
|
|
67
55
|
|
|
68
56
|
try {
|
|
69
57
|
await config.onPurchaseCompleted(userId, productId, customerInfo, source);
|
|
70
|
-
if (__DEV__) {
|
|
71
|
-
console.log('[PremiumStatusSyncer] onPurchaseCompleted callback executed successfully');
|
|
72
|
-
}
|
|
73
58
|
} catch (error) {
|
|
74
59
|
if (__DEV__) {
|
|
75
60
|
console.error('[PremiumStatusSyncer] onPurchaseCompleted callback failed:', error);
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
import { SubscriptionHeader } from "./components/SubscriptionHeader";
|
|
14
14
|
import { CreditsList, type CreditItem } from "./components/CreditsList";
|
|
15
15
|
import { UpgradePrompt, type Benefit } from "./components/UpgradePrompt";
|
|
16
|
-
import { DevTestSection, type DevTestActions } from "./components/DevTestSection";
|
|
17
16
|
|
|
18
17
|
export interface SubscriptionDisplayFlags {
|
|
19
18
|
showHeader: boolean;
|
|
@@ -39,15 +38,11 @@ export interface SubscriptionDetailTranslations {
|
|
|
39
38
|
upgradeButton: string;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
export interface DevToolsConfig {
|
|
43
|
-
actions: DevTestActions;
|
|
44
|
-
title?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
41
|
export interface UpgradePromptConfig {
|
|
48
42
|
title: string;
|
|
49
43
|
subtitle?: string;
|
|
50
44
|
benefits?: readonly Benefit[];
|
|
45
|
+
onUpgrade?: () => void;
|
|
51
46
|
}
|
|
52
47
|
|
|
53
48
|
export interface SubscriptionDetailConfig {
|
|
@@ -59,8 +54,7 @@ export interface SubscriptionDetailConfig {
|
|
|
59
54
|
daysRemaining?: number | null;
|
|
60
55
|
credits?: readonly CreditItem[];
|
|
61
56
|
translations: SubscriptionDetailTranslations;
|
|
62
|
-
upgradePrompt?: UpgradePromptConfig
|
|
63
|
-
devTools?: DevToolsConfig;
|
|
57
|
+
upgradePrompt?: UpgradePromptConfig;
|
|
64
58
|
}
|
|
65
59
|
|
|
66
60
|
export interface SubscriptionDetailScreenProps {
|
|
@@ -98,14 +92,6 @@ export const SubscriptionDetailScreen: React.FC<
|
|
|
98
92
|
edges={["bottom"]}
|
|
99
93
|
backgroundColor={tokens.colors.backgroundPrimary}
|
|
100
94
|
contentContainerStyle={styles.content}
|
|
101
|
-
footer={
|
|
102
|
-
config.devTools ? (
|
|
103
|
-
<DevTestSection
|
|
104
|
-
actions={config.devTools.actions}
|
|
105
|
-
title={config.devTools.title}
|
|
106
|
-
/>
|
|
107
|
-
) : undefined
|
|
108
|
-
}
|
|
109
95
|
>
|
|
110
96
|
<View style={styles.cardsContainer}>
|
|
111
97
|
{showHeader && (
|
|
@@ -49,7 +49,6 @@ export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set, get) =
|
|
|
49
49
|
// Still update to the new purchase to recover from potential stuck state
|
|
50
50
|
}
|
|
51
51
|
if (__DEV__) {
|
|
52
|
-
console.log("[PurchaseLoadingStore] startPurchase:", { productId, source });
|
|
53
52
|
}
|
|
54
53
|
set({
|
|
55
54
|
isPurchasing: true,
|
|
@@ -67,7 +66,6 @@ export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set, get) =
|
|
|
67
66
|
// Reset to initial state to recover from potential stuck state
|
|
68
67
|
}
|
|
69
68
|
if (__DEV__) {
|
|
70
|
-
console.log("[PurchaseLoadingStore] endPurchase");
|
|
71
69
|
}
|
|
72
70
|
set({
|
|
73
71
|
isPurchasing: false,
|
|
@@ -78,7 +76,6 @@ export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set, get) =
|
|
|
78
76
|
|
|
79
77
|
reset: () => {
|
|
80
78
|
if (__DEV__) {
|
|
81
|
-
console.log("[PurchaseLoadingStore] reset");
|
|
82
79
|
}
|
|
83
80
|
set(initialState);
|
|
84
81
|
},
|
|
@@ -64,48 +64,19 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
64
64
|
}, [requiredCredits]);
|
|
65
65
|
|
|
66
66
|
useEffect(() => {
|
|
67
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
68
|
-
console.log("[useFeatureGate] Auth credits effect", {
|
|
69
|
-
isWaiting: isWaitingForAuthCreditsRef.current,
|
|
70
|
-
isLoaded: isCreditsLoaded,
|
|
71
|
-
hasPending: !!pendingActionRef.current,
|
|
72
|
-
credits: creditBalance,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
67
|
if (!isWaitingForAuthCreditsRef.current || !isCreditsLoaded || !pendingActionRef.current) {
|
|
77
68
|
return;
|
|
78
69
|
}
|
|
79
70
|
|
|
80
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
81
|
-
console.log("[useFeatureGate] Credits loaded after auth", {
|
|
82
|
-
credits: creditBalance,
|
|
83
|
-
hasSubscription,
|
|
84
|
-
isCreditsLoaded,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
71
|
isWaitingForAuthCreditsRef.current = false;
|
|
89
72
|
|
|
90
73
|
if (hasSubscription || creditBalance >= requiredCredits) {
|
|
91
74
|
const action = pendingActionRef.current;
|
|
92
75
|
pendingActionRef.current = null;
|
|
93
|
-
|
|
94
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
95
|
-
console.log("[useFeatureGate] Proceeding with action after auth", {
|
|
96
|
-
credits: creditBalance,
|
|
97
|
-
hasSubscription,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
76
|
action();
|
|
101
77
|
return;
|
|
102
78
|
}
|
|
103
79
|
|
|
104
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
105
|
-
console.log("[useFeatureGate] No credits after auth, showing paywall", {
|
|
106
|
-
credits: creditBalance,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
80
|
isWaitingForPurchaseRef.current = true;
|
|
110
81
|
onShowPaywall(requiredCredits);
|
|
111
82
|
}, [isCreditsLoaded, creditBalance, hasSubscription, requiredCredits, onShowPaywall]);
|
|
@@ -119,10 +90,6 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
119
90
|
const action = pendingActionRef.current;
|
|
120
91
|
pendingActionRef.current = null;
|
|
121
92
|
isWaitingForPurchaseRef.current = false;
|
|
122
|
-
|
|
123
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
124
|
-
console.log("[useFeatureGate] Access acquired (credits or subscription), executing pending action");
|
|
125
|
-
}
|
|
126
93
|
action();
|
|
127
94
|
}
|
|
128
95
|
|
|
@@ -132,24 +99,10 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
132
99
|
|
|
133
100
|
const requireFeature = useCallback(
|
|
134
101
|
(action: () => void | Promise<void>) => {
|
|
135
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
136
|
-
console.log("[useFeatureGate] requireFeature", {
|
|
137
|
-
isAuthenticated,
|
|
138
|
-
hasSubscription: hasSubscriptionRef.current,
|
|
139
|
-
creditBalance: creditBalanceRef.current,
|
|
140
|
-
requiredCredits: requiredCreditsRef.current,
|
|
141
|
-
isCreditsLoaded,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
102
|
if (!isAuthenticated) {
|
|
146
103
|
const postAuthAction = () => {
|
|
147
104
|
pendingActionRef.current = action;
|
|
148
105
|
isWaitingForAuthCreditsRef.current = true;
|
|
149
|
-
|
|
150
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
151
|
-
console.log("[useFeatureGate] Auth completed, waiting for credits to load");
|
|
152
|
-
}
|
|
153
106
|
};
|
|
154
107
|
onShowAuthModal(postAuthAction);
|
|
155
108
|
return;
|
|
@@ -166,9 +119,6 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
166
119
|
}
|
|
167
120
|
|
|
168
121
|
if (currentBalance < currentRequiredCredits) {
|
|
169
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
170
|
-
console.log("[useFeatureGate] No credits, showing paywall");
|
|
171
|
-
}
|
|
172
122
|
pendingActionRef.current = action;
|
|
173
123
|
isWaitingForPurchaseRef.current = true;
|
|
174
124
|
onShowPaywallRef.current(currentRequiredCredits);
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dev Test Section
|
|
3
|
-
* Developer testing tools for subscription renewals
|
|
4
|
-
* Only visible in __DEV__ mode
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import React, { useMemo } from "react";
|
|
8
|
-
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
9
|
-
import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
10
|
-
|
|
11
|
-
export interface DevTestActions {
|
|
12
|
-
onTestRenewal: () => void;
|
|
13
|
-
onCheckCredits: () => void;
|
|
14
|
-
onTestDuplicate: () => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface DevTestSectionProps {
|
|
18
|
-
actions: DevTestActions;
|
|
19
|
-
title?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Dev test button translations */
|
|
23
|
-
export interface DevTestTranslations {
|
|
24
|
-
title: string;
|
|
25
|
-
testRenewal: string;
|
|
26
|
-
checkCredits: string;
|
|
27
|
-
testDuplicate: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface DevTestSectionWithTranslationsProps extends DevTestSectionProps {
|
|
31
|
-
translations?: DevTestTranslations;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const DevTestSection: React.FC<DevTestSectionWithTranslationsProps> = ({
|
|
35
|
-
actions,
|
|
36
|
-
title,
|
|
37
|
-
translations,
|
|
38
|
-
}) => {
|
|
39
|
-
const tokens = useAppDesignTokens();
|
|
40
|
-
|
|
41
|
-
const styles = useMemo(
|
|
42
|
-
() =>
|
|
43
|
-
StyleSheet.create({
|
|
44
|
-
container: {
|
|
45
|
-
padding: tokens.spacing.lg,
|
|
46
|
-
gap: tokens.spacing.md,
|
|
47
|
-
borderTopWidth: 1,
|
|
48
|
-
backgroundColor: tokens.colors.surfaceSecondary,
|
|
49
|
-
borderTopColor: tokens.colors.border,
|
|
50
|
-
},
|
|
51
|
-
title: {
|
|
52
|
-
fontWeight: "600",
|
|
53
|
-
marginBottom: tokens.spacing.xs,
|
|
54
|
-
},
|
|
55
|
-
button: {
|
|
56
|
-
paddingVertical: tokens.spacing.md,
|
|
57
|
-
paddingHorizontal: tokens.spacing.lg,
|
|
58
|
-
borderRadius: tokens.radius.md,
|
|
59
|
-
alignItems: "center",
|
|
60
|
-
},
|
|
61
|
-
primaryButton: {
|
|
62
|
-
backgroundColor: tokens.colors.primary,
|
|
63
|
-
},
|
|
64
|
-
secondaryButton: {
|
|
65
|
-
backgroundColor: tokens.colors.surfaceSecondary,
|
|
66
|
-
borderWidth: 1,
|
|
67
|
-
borderColor: tokens.colors.border,
|
|
68
|
-
},
|
|
69
|
-
buttonText: {
|
|
70
|
-
fontWeight: "500",
|
|
71
|
-
},
|
|
72
|
-
}),
|
|
73
|
-
[tokens]
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
if (!__DEV__) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const displayTitle = title || translations?.title;
|
|
81
|
-
const renewalText = translations?.testRenewal || "Test Auto-Renewal";
|
|
82
|
-
const creditsText = translations?.checkCredits || "Check Credits";
|
|
83
|
-
const duplicateText = translations?.testDuplicate || "Test Duplicate Protection";
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<View style={styles.container}>
|
|
87
|
-
{displayTitle && (
|
|
88
|
-
<AtomicText
|
|
89
|
-
type="titleMedium"
|
|
90
|
-
style={[styles.title, { color: tokens.colors.textPrimary }]}
|
|
91
|
-
>
|
|
92
|
-
{displayTitle}
|
|
93
|
-
</AtomicText>
|
|
94
|
-
)}
|
|
95
|
-
|
|
96
|
-
<TouchableOpacity
|
|
97
|
-
style={[styles.button, styles.primaryButton]}
|
|
98
|
-
onPress={actions.onTestRenewal}
|
|
99
|
-
>
|
|
100
|
-
<AtomicText
|
|
101
|
-
type="bodyMedium"
|
|
102
|
-
style={[styles.buttonText, { color: tokens.colors.onPrimary }]}
|
|
103
|
-
>
|
|
104
|
-
{renewalText}
|
|
105
|
-
</AtomicText>
|
|
106
|
-
</TouchableOpacity>
|
|
107
|
-
|
|
108
|
-
<TouchableOpacity
|
|
109
|
-
style={[styles.button, styles.secondaryButton]}
|
|
110
|
-
onPress={actions.onCheckCredits}
|
|
111
|
-
>
|
|
112
|
-
<AtomicText
|
|
113
|
-
type="bodyMedium"
|
|
114
|
-
style={[styles.buttonText, { color: tokens.colors.textPrimary }]}
|
|
115
|
-
>
|
|
116
|
-
{creditsText}
|
|
117
|
-
</AtomicText>
|
|
118
|
-
</TouchableOpacity>
|
|
119
|
-
|
|
120
|
-
<TouchableOpacity
|
|
121
|
-
style={[styles.button, styles.secondaryButton]}
|
|
122
|
-
onPress={actions.onTestDuplicate}
|
|
123
|
-
>
|
|
124
|
-
<AtomicText
|
|
125
|
-
type="bodyMedium"
|
|
126
|
-
style={[styles.buttonText, { color: tokens.colors.textPrimary }]}
|
|
127
|
-
>
|
|
128
|
-
{duplicateText}
|
|
129
|
-
</AtomicText>
|
|
130
|
-
</TouchableOpacity>
|
|
131
|
-
</View>
|
|
132
|
-
);
|
|
133
|
-
};
|