@umituz/react-native-subscription 2.20.7 → 2.22.0
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/paywall/components/PaywallContainer.tsx +1 -1
- package/src/domains/paywall/components/PaywallModal.tsx +50 -7
- package/src/index.ts +11 -0
- package/src/infrastructure/services/FeedbackService.ts +117 -0
- package/src/presentation/components/overlay/PurchaseLoadingOverlay.tsx +63 -0
- package/src/presentation/components/overlay/index.ts +5 -0
- package/src/presentation/hooks/feedback/useFeedbackSubmit.ts +105 -0
- package/src/presentation/hooks/index.ts +1 -0
- package/src/presentation/hooks/useSavedPurchaseAutoExecution.ts +33 -7
- package/src/presentation/stores/index.ts +13 -0
- package/src/presentation/stores/purchaseLoadingStore.ts +73 -0
- package/src/revenuecat/infrastructure/services/PurchaseHandler.ts +16 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.22.0",
|
|
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",
|
|
@@ -13,7 +13,7 @@ import { usePaywallActions } from "../hooks/usePaywallActions";
|
|
|
13
13
|
import type { PaywallContainerProps } from "./PaywallContainer.types";
|
|
14
14
|
|
|
15
15
|
export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
16
|
-
const { userId,
|
|
16
|
+
const { userId, translations, mode = "subscription", legalUrls, features, heroImage, bestValueIdentifier, creditsLabel, creditAmounts, packageFilterConfig, source, onPurchaseSuccess, onPurchaseError, onAuthRequired, visible, onClose } = props;
|
|
17
17
|
|
|
18
18
|
const { showPaywall, closePaywall, currentSource } = usePaywallVisibility();
|
|
19
19
|
const isVisible = visible ?? showPaywall;
|
|
@@ -12,6 +12,9 @@ import type { SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from
|
|
|
12
12
|
import { paywallModalStyles as styles } from "./PaywallModal.styles";
|
|
13
13
|
import { PaywallFeatures } from "./PaywallFeatures";
|
|
14
14
|
import { PaywallFooter } from "./PaywallFooter";
|
|
15
|
+
import { usePurchaseLoadingStore, selectIsPurchasing } from "../../../presentation/stores";
|
|
16
|
+
|
|
17
|
+
declare const __DEV__: boolean;
|
|
15
18
|
|
|
16
19
|
export interface PaywallModalProps {
|
|
17
20
|
visible: boolean;
|
|
@@ -33,21 +36,61 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
33
36
|
const { visible, onClose, translations, packages = [], features = [], isLoading = false, legalUrls = {}, bestValueIdentifier, creditAmounts, creditsLabel, heroImage, onPurchase, onRestore } = props;
|
|
34
37
|
const tokens = useAppDesignTokens();
|
|
35
38
|
const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
|
|
36
|
-
const [
|
|
39
|
+
const [isLocalProcessing, setIsLocalProcessing] = useState(false);
|
|
40
|
+
|
|
41
|
+
// Global purchase loading state (for auto-execution after auth)
|
|
42
|
+
const isGlobalPurchasing = usePurchaseLoadingStore(selectIsPurchasing);
|
|
43
|
+
const { startPurchase, endPurchase } = usePurchaseLoadingStore();
|
|
44
|
+
|
|
45
|
+
// Combined processing state
|
|
46
|
+
const isProcessing = isLocalProcessing || isGlobalPurchasing;
|
|
37
47
|
|
|
38
48
|
const handlePurchase = useCallback(async () => {
|
|
39
49
|
if (!selectedPlanId || !onPurchase) return;
|
|
40
|
-
|
|
50
|
+
|
|
51
|
+
if (__DEV__) {
|
|
52
|
+
console.log("[PaywallModal] handlePurchase starting:", { selectedPlanId });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setIsLocalProcessing(true);
|
|
56
|
+
startPurchase(selectedPlanId, "manual");
|
|
57
|
+
|
|
41
58
|
try {
|
|
42
59
|
const pkg = packages.find((p) => p.product.identifier === selectedPlanId);
|
|
43
|
-
if (pkg)
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
if (pkg) {
|
|
61
|
+
if (__DEV__) {
|
|
62
|
+
console.log("[PaywallModal] Calling onPurchase:", { productId: pkg.product.identifier });
|
|
63
|
+
}
|
|
64
|
+
await onPurchase(pkg);
|
|
65
|
+
if (__DEV__) {
|
|
66
|
+
console.log("[PaywallModal] onPurchase completed");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
setIsLocalProcessing(false);
|
|
71
|
+
endPurchase();
|
|
72
|
+
if (__DEV__) {
|
|
73
|
+
console.log("[PaywallModal] handlePurchase finished");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}, [selectedPlanId, packages, onPurchase, startPurchase, endPurchase]);
|
|
46
77
|
|
|
47
78
|
const handleRestore = useCallback(async () => {
|
|
48
79
|
if (!onRestore || isProcessing) return;
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
|
|
81
|
+
if (__DEV__) {
|
|
82
|
+
console.log("[PaywallModal] handleRestore starting");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setIsLocalProcessing(true);
|
|
86
|
+
try {
|
|
87
|
+
await onRestore();
|
|
88
|
+
if (__DEV__) {
|
|
89
|
+
console.log("[PaywallModal] handleRestore completed");
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
setIsLocalProcessing(false);
|
|
93
|
+
}
|
|
51
94
|
}, [onRestore, isProcessing]);
|
|
52
95
|
|
|
53
96
|
const handleLegalUrl = useCallback(async (url: string | undefined) => {
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,13 @@ export type { ISubscriptionRepository } from "./application/ports/ISubscriptionR
|
|
|
14
14
|
|
|
15
15
|
// Infrastructure Layer
|
|
16
16
|
export { SubscriptionService, initializeSubscriptionService } from "./infrastructure/services/SubscriptionService";
|
|
17
|
+
export {
|
|
18
|
+
submitFeedback,
|
|
19
|
+
submitPaywallFeedback,
|
|
20
|
+
submitSettingsFeedback,
|
|
21
|
+
type FeedbackData,
|
|
22
|
+
type FeedbackSubmitResult,
|
|
23
|
+
} from "./infrastructure/services/FeedbackService";
|
|
17
24
|
export { initializeSubscription, type SubscriptionInitConfig, type CreditPackageConfig } from "./infrastructure/services/SubscriptionInitializer";
|
|
18
25
|
export { CreditsRepository, createCreditsRepository } from "./infrastructure/repositories/CreditsRepository";
|
|
19
26
|
export { configureCreditsRepository, getCreditsRepository, getCreditsConfig, resetCreditsRepository, isCreditsRepositoryConfigured } from "./infrastructure/repositories/CreditsRepositoryProvider";
|
|
@@ -32,9 +39,13 @@ export * from "./presentation/components/details/PremiumDetailsCard";
|
|
|
32
39
|
export * from "./presentation/components/details/PremiumStatusBadge";
|
|
33
40
|
export * from "./presentation/components/sections/SubscriptionSection";
|
|
34
41
|
export * from "./presentation/components/feedback/PaywallFeedbackModal";
|
|
42
|
+
export * from "./presentation/components/overlay";
|
|
35
43
|
export * from "./presentation/screens/SubscriptionDetailScreen";
|
|
36
44
|
export * from "./presentation/types/SubscriptionDetailTypes";
|
|
37
45
|
|
|
46
|
+
// Presentation Layer - Stores
|
|
47
|
+
export * from "./presentation/stores";
|
|
48
|
+
|
|
38
49
|
// Credits Domain
|
|
39
50
|
export type {
|
|
40
51
|
CreditType,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Service
|
|
3
|
+
* Handles feedback submission to Firestore
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getFirestore } from "@umituz/react-native-firebase";
|
|
7
|
+
import { collection, addDoc } from "firebase/firestore";
|
|
8
|
+
|
|
9
|
+
export interface FeedbackData {
|
|
10
|
+
userId: string | null;
|
|
11
|
+
userEmail: string | null;
|
|
12
|
+
type: string;
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
rating?: number;
|
|
16
|
+
status?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FeedbackSubmitResult {
|
|
20
|
+
success: boolean;
|
|
21
|
+
error?: Error;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FEEDBACK_COLLECTION = "feedback";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Submit feedback to Firestore
|
|
28
|
+
*/
|
|
29
|
+
export async function submitFeedback(
|
|
30
|
+
data: FeedbackData
|
|
31
|
+
): Promise<FeedbackSubmitResult> {
|
|
32
|
+
const db = getFirestore();
|
|
33
|
+
|
|
34
|
+
if (!db) {
|
|
35
|
+
if (__DEV__) {
|
|
36
|
+
console.warn("[FeedbackService] Firestore not available");
|
|
37
|
+
}
|
|
38
|
+
return { success: false, error: new Error("Firestore not available") };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
if (__DEV__) {
|
|
43
|
+
console.log("[FeedbackService] Submitting feedback:", {
|
|
44
|
+
type: data.type,
|
|
45
|
+
userId: data.userId?.slice(0, 8),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const now = new Date().toISOString();
|
|
50
|
+
|
|
51
|
+
await addDoc(collection(db, FEEDBACK_COLLECTION), {
|
|
52
|
+
userId: data.userId,
|
|
53
|
+
userEmail: data.userEmail,
|
|
54
|
+
type: data.type,
|
|
55
|
+
title: data.title,
|
|
56
|
+
description: data.description,
|
|
57
|
+
rating: data.rating ?? null,
|
|
58
|
+
status: data.status ?? "pending",
|
|
59
|
+
createdAt: now,
|
|
60
|
+
updatedAt: now,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (__DEV__) {
|
|
64
|
+
console.log("[FeedbackService] Feedback submitted successfully");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { success: true };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (__DEV__) {
|
|
70
|
+
console.error("[FeedbackService] Submit error:", error);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
error: error instanceof Error ? error : new Error("Unknown error"),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Submit paywall decline feedback
|
|
81
|
+
*/
|
|
82
|
+
export async function submitPaywallFeedback(
|
|
83
|
+
userId: string | null,
|
|
84
|
+
userEmail: string | null,
|
|
85
|
+
reason: string
|
|
86
|
+
): Promise<FeedbackSubmitResult> {
|
|
87
|
+
return submitFeedback({
|
|
88
|
+
userId,
|
|
89
|
+
userEmail,
|
|
90
|
+
type: "paywall_declined",
|
|
91
|
+
title: "Paywall Declined",
|
|
92
|
+
description: reason,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Submit general settings feedback
|
|
98
|
+
*/
|
|
99
|
+
export async function submitSettingsFeedback(
|
|
100
|
+
userId: string | null,
|
|
101
|
+
userEmail: string | null,
|
|
102
|
+
data: {
|
|
103
|
+
type?: string;
|
|
104
|
+
title?: string;
|
|
105
|
+
description: string;
|
|
106
|
+
rating?: number;
|
|
107
|
+
}
|
|
108
|
+
): Promise<FeedbackSubmitResult> {
|
|
109
|
+
return submitFeedback({
|
|
110
|
+
userId,
|
|
111
|
+
userEmail,
|
|
112
|
+
type: data.type ?? "general",
|
|
113
|
+
title: data.title ?? "Settings Feedback",
|
|
114
|
+
description: data.description,
|
|
115
|
+
rating: data.rating,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purchase Loading Overlay
|
|
3
|
+
* Full-screen overlay shown during purchase operations
|
|
4
|
+
* Locks the UI and shows a spinner with optional message
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, Modal, StyleSheet } from "react-native";
|
|
9
|
+
import { AtomicSpinner, AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
10
|
+
import { usePurchaseLoadingStore, selectIsPurchasing } from "../../stores";
|
|
11
|
+
|
|
12
|
+
export interface PurchaseLoadingOverlayProps {
|
|
13
|
+
/** Loading message to display */
|
|
14
|
+
loadingText?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const PurchaseLoadingOverlay: React.FC<PurchaseLoadingOverlayProps> = React.memo(
|
|
18
|
+
({ loadingText }) => {
|
|
19
|
+
const tokens = useAppDesignTokens();
|
|
20
|
+
const isPurchasing = usePurchaseLoadingStore(selectIsPurchasing);
|
|
21
|
+
|
|
22
|
+
if (!isPurchasing) return null;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Modal visible transparent animationType="fade" statusBarTranslucent>
|
|
26
|
+
<View style={[styles.container, { backgroundColor: "rgba(0, 0, 0, 0.7)" }]}>
|
|
27
|
+
<View style={[styles.content, { backgroundColor: tokens.colors.surface }]}>
|
|
28
|
+
<AtomicSpinner size="lg" color="primary" />
|
|
29
|
+
{loadingText && (
|
|
30
|
+
<AtomicText
|
|
31
|
+
type="bodyLarge"
|
|
32
|
+
style={[styles.text, { color: tokens.colors.textPrimary }]}
|
|
33
|
+
>
|
|
34
|
+
{loadingText}
|
|
35
|
+
</AtomicText>
|
|
36
|
+
)}
|
|
37
|
+
</View>
|
|
38
|
+
</View>
|
|
39
|
+
</Modal>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
PurchaseLoadingOverlay.displayName = "PurchaseLoadingOverlay";
|
|
45
|
+
|
|
46
|
+
const styles = StyleSheet.create({
|
|
47
|
+
container: {
|
|
48
|
+
flex: 1,
|
|
49
|
+
justifyContent: "center",
|
|
50
|
+
alignItems: "center",
|
|
51
|
+
},
|
|
52
|
+
content: {
|
|
53
|
+
paddingHorizontal: 32,
|
|
54
|
+
paddingVertical: 24,
|
|
55
|
+
borderRadius: 16,
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
minWidth: 200,
|
|
58
|
+
},
|
|
59
|
+
text: {
|
|
60
|
+
marginTop: 16,
|
|
61
|
+
textAlign: "center",
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Submit Hooks
|
|
3
|
+
* React hooks for submitting feedback to Firestore
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import { useAuth } from "@umituz/react-native-auth";
|
|
8
|
+
import {
|
|
9
|
+
submitPaywallFeedback,
|
|
10
|
+
submitSettingsFeedback,
|
|
11
|
+
} from "../../../infrastructure/services/FeedbackService";
|
|
12
|
+
|
|
13
|
+
export interface UsePaywallFeedbackSubmitOptions {
|
|
14
|
+
onSuccess?: () => void;
|
|
15
|
+
onError?: (error: Error) => void;
|
|
16
|
+
onComplete?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hook for submitting paywall decline feedback
|
|
21
|
+
*/
|
|
22
|
+
export function usePaywallFeedbackSubmit(
|
|
23
|
+
options: UsePaywallFeedbackSubmitOptions = {}
|
|
24
|
+
) {
|
|
25
|
+
const { user } = useAuth();
|
|
26
|
+
const { onSuccess, onError, onComplete } = options;
|
|
27
|
+
|
|
28
|
+
const submit = useCallback(
|
|
29
|
+
async (reason: string) => {
|
|
30
|
+
if (__DEV__) {
|
|
31
|
+
console.log("[usePaywallFeedbackSubmit] Submitting:", {
|
|
32
|
+
userId: user?.uid?.slice(0, 8),
|
|
33
|
+
reason: reason.slice(0, 20),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await submitPaywallFeedback(
|
|
38
|
+
user?.uid ?? null,
|
|
39
|
+
user?.email ?? null,
|
|
40
|
+
reason
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (result.success) {
|
|
44
|
+
onSuccess?.();
|
|
45
|
+
} else if (result.error) {
|
|
46
|
+
onError?.(result.error);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onComplete?.();
|
|
50
|
+
},
|
|
51
|
+
[user, onSuccess, onError, onComplete]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return { submit };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SettingsFeedbackData {
|
|
58
|
+
type?: string;
|
|
59
|
+
title?: string;
|
|
60
|
+
description: string;
|
|
61
|
+
rating?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface UseSettingsFeedbackSubmitOptions {
|
|
65
|
+
onSuccess?: () => void;
|
|
66
|
+
onError?: (error: Error) => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hook for submitting general settings feedback
|
|
71
|
+
*/
|
|
72
|
+
export function useSettingsFeedbackSubmit(
|
|
73
|
+
options: UseSettingsFeedbackSubmitOptions = {}
|
|
74
|
+
) {
|
|
75
|
+
const { user } = useAuth();
|
|
76
|
+
const { onSuccess, onError } = options;
|
|
77
|
+
|
|
78
|
+
const submit = useCallback(
|
|
79
|
+
async (data: SettingsFeedbackData) => {
|
|
80
|
+
if (__DEV__) {
|
|
81
|
+
console.log("[useSettingsFeedbackSubmit] Submitting:", {
|
|
82
|
+
userId: user?.uid?.slice(0, 8),
|
|
83
|
+
type: data.type,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = await submitSettingsFeedback(
|
|
88
|
+
user?.uid ?? null,
|
|
89
|
+
user?.email ?? null,
|
|
90
|
+
data
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (result.success) {
|
|
94
|
+
onSuccess?.();
|
|
95
|
+
} else if (result.error) {
|
|
96
|
+
onError?.(result.error);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
},
|
|
101
|
+
[user, onSuccess, onError]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return { submit };
|
|
105
|
+
}
|
|
@@ -7,6 +7,7 @@ import { useEffect, useRef, useCallback } from "react";
|
|
|
7
7
|
import { getSavedPurchase, clearSavedPurchase } from "./useAuthAwarePurchase";
|
|
8
8
|
import { usePremium } from "./usePremium";
|
|
9
9
|
import { SubscriptionManager } from "../../revenuecat";
|
|
10
|
+
import { usePurchaseLoadingStore } from "../stores";
|
|
10
11
|
|
|
11
12
|
declare const __DEV__: boolean;
|
|
12
13
|
|
|
@@ -26,6 +27,7 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
26
27
|
): UseSavedPurchaseAutoExecutionResult => {
|
|
27
28
|
const { userId, isAnonymous, onSuccess, onError } = params;
|
|
28
29
|
const { purchasePackage } = usePremium(userId);
|
|
30
|
+
const { startPurchase, endPurchase } = usePurchaseLoadingStore();
|
|
29
31
|
|
|
30
32
|
const prevIsAnonymousRef = useRef<boolean | undefined>(undefined);
|
|
31
33
|
const isExecutingRef = useRef(false);
|
|
@@ -36,7 +38,8 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
36
38
|
|
|
37
39
|
if (__DEV__) {
|
|
38
40
|
console.log(
|
|
39
|
-
"[SavedPurchaseAutoExecution] Waiting for RevenueCat initialization..."
|
|
41
|
+
"[SavedPurchaseAutoExecution] Waiting for RevenueCat initialization...",
|
|
42
|
+
{ userId: userId.slice(0, 8), productId: savedPurchase.pkg.product.identifier }
|
|
40
43
|
);
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -60,16 +63,26 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
60
63
|
|
|
61
64
|
if (__DEV__) {
|
|
62
65
|
console.log(
|
|
63
|
-
"[SavedPurchaseAutoExecution] RevenueCat ready,
|
|
64
|
-
pkg.product.identifier
|
|
66
|
+
"[SavedPurchaseAutoExecution] RevenueCat ready, starting purchase...",
|
|
67
|
+
{ productId: pkg.product.identifier, userId: userId.slice(0, 8) }
|
|
65
68
|
);
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
// Start global loading state
|
|
72
|
+
startPurchase(pkg.product.identifier, "auto-execution");
|
|
73
|
+
|
|
68
74
|
try {
|
|
75
|
+
if (__DEV__) {
|
|
76
|
+
console.log("[SavedPurchaseAutoExecution] Calling purchasePackage...");
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
const success = await purchasePackage(pkg);
|
|
70
80
|
|
|
71
81
|
if (__DEV__) {
|
|
72
|
-
console.log("[SavedPurchaseAutoExecution] Purchase
|
|
82
|
+
console.log("[SavedPurchaseAutoExecution] Purchase completed:", {
|
|
83
|
+
success,
|
|
84
|
+
productId: pkg.product.identifier
|
|
85
|
+
});
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
if (success && onSuccess) {
|
|
@@ -77,13 +90,22 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
77
90
|
}
|
|
78
91
|
} catch (error) {
|
|
79
92
|
if (__DEV__) {
|
|
80
|
-
console.error("[SavedPurchaseAutoExecution] Purchase
|
|
93
|
+
console.error("[SavedPurchaseAutoExecution] Purchase error:", {
|
|
94
|
+
error,
|
|
95
|
+
productId: pkg.product.identifier
|
|
96
|
+
});
|
|
81
97
|
}
|
|
82
98
|
if (onError && error instanceof Error) {
|
|
83
99
|
onError(error);
|
|
84
100
|
}
|
|
85
101
|
} finally {
|
|
102
|
+
// End global loading state
|
|
103
|
+
endPurchase();
|
|
86
104
|
isExecutingRef.current = false;
|
|
105
|
+
|
|
106
|
+
if (__DEV__) {
|
|
107
|
+
console.log("[SavedPurchaseAutoExecution] Purchase flow finished");
|
|
108
|
+
}
|
|
87
109
|
}
|
|
88
110
|
|
|
89
111
|
return;
|
|
@@ -99,7 +121,7 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
99
121
|
}
|
|
100
122
|
clearSavedPurchase();
|
|
101
123
|
isExecutingRef.current = false;
|
|
102
|
-
}, [userId, purchasePackage, onSuccess, onError]);
|
|
124
|
+
}, [userId, purchasePackage, onSuccess, onError, startPurchase, endPurchase]);
|
|
103
125
|
|
|
104
126
|
useEffect(() => {
|
|
105
127
|
const isAuthenticated = !!userId && !isAnonymous;
|
|
@@ -110,7 +132,7 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
110
132
|
const becameAuthenticated = wasAnonymous && isAuthenticated;
|
|
111
133
|
|
|
112
134
|
if (__DEV__) {
|
|
113
|
-
console.log("[SavedPurchaseAutoExecution]
|
|
135
|
+
console.log("[SavedPurchaseAutoExecution] Auth state check:", {
|
|
114
136
|
userId: userId?.slice(0, 8),
|
|
115
137
|
prevIsAnonymous,
|
|
116
138
|
isAnonymous,
|
|
@@ -118,11 +140,15 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
118
140
|
wasAnonymous,
|
|
119
141
|
becameAuthenticated,
|
|
120
142
|
hasSavedPurchase: !!savedPurchase,
|
|
143
|
+
savedProductId: savedPurchase?.pkg.product.identifier,
|
|
121
144
|
willExecute: becameAuthenticated && !!savedPurchase && !isExecutingRef.current,
|
|
122
145
|
});
|
|
123
146
|
}
|
|
124
147
|
|
|
125
148
|
if (becameAuthenticated && savedPurchase && !isExecutingRef.current) {
|
|
149
|
+
if (__DEV__) {
|
|
150
|
+
console.log("[SavedPurchaseAutoExecution] Triggering auto-execution...");
|
|
151
|
+
}
|
|
126
152
|
executeWithWait();
|
|
127
153
|
}
|
|
128
154
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation Layer - Stores
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
usePurchaseLoadingStore,
|
|
7
|
+
selectIsPurchasing,
|
|
8
|
+
selectPurchasingProductId,
|
|
9
|
+
selectPurchaseSource,
|
|
10
|
+
type PurchaseLoadingState,
|
|
11
|
+
type PurchaseLoadingActions,
|
|
12
|
+
type PurchaseLoadingStore,
|
|
13
|
+
} from "./purchaseLoadingStore";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purchase Loading Store
|
|
3
|
+
* Global state for tracking purchase loading across the app
|
|
4
|
+
* Used by both PaywallModal and useSavedPurchaseAutoExecution
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { create } from "zustand";
|
|
8
|
+
|
|
9
|
+
declare const __DEV__: boolean;
|
|
10
|
+
|
|
11
|
+
export interface PurchaseLoadingState {
|
|
12
|
+
/** Whether a purchase is in progress */
|
|
13
|
+
isPurchasing: boolean;
|
|
14
|
+
/** Product identifier being purchased */
|
|
15
|
+
purchasingProductId: string | null;
|
|
16
|
+
/** Source of the purchase (manual, auto-execution, etc.) */
|
|
17
|
+
purchaseSource: "manual" | "auto-execution" | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PurchaseLoadingActions {
|
|
21
|
+
/** Start purchase loading state */
|
|
22
|
+
startPurchase: (productId: string, source: "manual" | "auto-execution") => void;
|
|
23
|
+
/** End purchase loading state */
|
|
24
|
+
endPurchase: () => void;
|
|
25
|
+
/** Reset all state */
|
|
26
|
+
reset: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type PurchaseLoadingStore = PurchaseLoadingState & PurchaseLoadingActions;
|
|
30
|
+
|
|
31
|
+
const initialState: PurchaseLoadingState = {
|
|
32
|
+
isPurchasing: false,
|
|
33
|
+
purchasingProductId: null,
|
|
34
|
+
purchaseSource: null,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set) => ({
|
|
38
|
+
...initialState,
|
|
39
|
+
|
|
40
|
+
startPurchase: (productId, source) => {
|
|
41
|
+
if (__DEV__) {
|
|
42
|
+
console.log("[PurchaseLoadingStore] startPurchase:", { productId, source });
|
|
43
|
+
}
|
|
44
|
+
set({
|
|
45
|
+
isPurchasing: true,
|
|
46
|
+
purchasingProductId: productId,
|
|
47
|
+
purchaseSource: source,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
endPurchase: () => {
|
|
52
|
+
if (__DEV__) {
|
|
53
|
+
console.log("[PurchaseLoadingStore] endPurchase");
|
|
54
|
+
}
|
|
55
|
+
set({
|
|
56
|
+
isPurchasing: false,
|
|
57
|
+
purchasingProductId: null,
|
|
58
|
+
purchaseSource: null,
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
reset: () => {
|
|
63
|
+
if (__DEV__) {
|
|
64
|
+
console.log("[PurchaseLoadingStore] reset");
|
|
65
|
+
}
|
|
66
|
+
set(initialState);
|
|
67
|
+
},
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
// Selectors for optimized re-renders
|
|
71
|
+
export const selectIsPurchasing = (state: PurchaseLoadingStore) => state.isPurchasing;
|
|
72
|
+
export const selectPurchasingProductId = (state: PurchaseLoadingStore) => state.purchasingProductId;
|
|
73
|
+
export const selectPurchaseSource = (state: PurchaseLoadingStore) => state.purchaseSource;
|
|
@@ -65,11 +65,26 @@ export async function handlePurchase(
|
|
|
65
65
|
|
|
66
66
|
try {
|
|
67
67
|
if (__DEV__) {
|
|
68
|
-
console.log('[DEBUG PurchaseHandler] Calling Purchases.purchasePackage...'
|
|
68
|
+
console.log('[DEBUG PurchaseHandler] Calling Purchases.purchasePackage...', {
|
|
69
|
+
productId: pkg.product.identifier,
|
|
70
|
+
packageIdentifier: pkg.identifier,
|
|
71
|
+
offeringIdentifier: pkg.offeringIdentifier,
|
|
72
|
+
timestamp: new Date().toISOString()
|
|
73
|
+
});
|
|
69
74
|
}
|
|
75
|
+
|
|
76
|
+
const startTime = Date.now();
|
|
70
77
|
const purchaseResult = await Purchases.purchasePackage(pkg);
|
|
78
|
+
const duration = Date.now() - startTime;
|
|
71
79
|
const customerInfo = purchaseResult.customerInfo;
|
|
72
80
|
|
|
81
|
+
if (__DEV__) {
|
|
82
|
+
console.log('[DEBUG PurchaseHandler] Purchases.purchasePackage returned', {
|
|
83
|
+
duration: `${duration}ms`,
|
|
84
|
+
productId: pkg.product.identifier
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
if (__DEV__) {
|
|
74
89
|
console.log('[DEBUG PurchaseHandler] Purchase completed', {
|
|
75
90
|
productId: pkg.product.identifier,
|