@umituz/react-native-subscription 2.27.113 → 2.27.115
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/application/CreditsInitializer.ts +27 -116
- package/src/domains/credits/application/credit-strategies/CreditAllocationOrchestrator.ts +1 -6
- package/src/domains/credits/application/creditDocumentHelpers.ts +58 -0
- package/src/domains/credits/application/creditOperationUtils.ts +154 -0
- package/src/domains/credits/presentation/useCredits.ts +1 -2
- package/src/domains/paywall/hooks/usePaywallActions.ts +0 -3
- package/src/domains/subscription/application/SubscriptionSyncService.ts +19 -20
- package/src/domains/subscription/core/RevenueCatError.ts +40 -31
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +0 -1
- package/src/domains/subscription/infrastructure/hooks/useRevenueCatTrialEligibility.ts +19 -85
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +33 -75
- package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +57 -0
- package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +6 -12
- package/src/domains/subscription/infrastructure/services/PurchaseHandler.ts +0 -2
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +3 -4
- package/src/domains/subscription/infrastructure/services/RevenueCatService.ts +2 -5
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +6 -12
- package/src/domains/subscription/infrastructure/utils/authPurchaseState.ts +69 -0
- package/src/domains/subscription/infrastructure/utils/trialEligibilityUtils.ts +77 -0
- package/src/domains/subscription/presentation/components/feedback/FeedbackOption.tsx +139 -0
- package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackModal.tsx +15 -70
- package/src/domains/subscription/presentation/components/feedback/paywallFeedbackStyles.ts +0 -92
- package/src/domains/subscription/presentation/stores/purchaseLoadingStore.ts +1 -18
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +22 -76
- package/src/domains/subscription/presentation/usePremium.ts +2 -11
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +1 -6
- package/src/domains/trial/application/TrialService.ts +4 -8
- package/src/domains/wallet/infrastructure/services/ProductMetadataService.ts +0 -13
- package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +0 -10
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +0 -8
- package/src/init/createSubscriptionInitModule.ts +1 -4
- package/src/presentation/hooks/feedback/useFeedbackSubmit.ts +0 -14
- package/src/shared/application/FeedbackService.ts +0 -21
- package/src/shared/infrastructure/SubscriptionEventBus.ts +2 -2
- package/src/shared/types/CommonTypes.ts +65 -0
- package/src/shared/utils/BaseError.ts +26 -0
- package/src/shared/utils/Logger.ts +14 -45
- package/src/shared/utils/SubscriptionError.ts +20 -30
- package/src/utils/packageTypeDetector.ts +0 -4
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Option Component
|
|
3
|
+
* Single feedback option with radio button and optional text input
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, TextInput, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
|
|
10
|
+
interface FeedbackOptionProps {
|
|
11
|
+
isSelected: boolean;
|
|
12
|
+
text: string;
|
|
13
|
+
showInput: boolean;
|
|
14
|
+
placeholder: string;
|
|
15
|
+
inputValue: string;
|
|
16
|
+
onSelect: () => void;
|
|
17
|
+
onChangeText: (text: string) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const FeedbackOption: React.FC<FeedbackOptionProps> = React.memo(({
|
|
21
|
+
isSelected,
|
|
22
|
+
text,
|
|
23
|
+
showInput,
|
|
24
|
+
placeholder,
|
|
25
|
+
inputValue,
|
|
26
|
+
onSelect,
|
|
27
|
+
onChangeText,
|
|
28
|
+
}) => {
|
|
29
|
+
const tokens = useAppDesignTokens();
|
|
30
|
+
|
|
31
|
+
const containerStyle = {
|
|
32
|
+
marginBottom: tokens.spacing.sm,
|
|
33
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
34
|
+
borderRadius: tokens.borderRadius.md,
|
|
35
|
+
overflow: "hidden" as const,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View style={containerStyle}>
|
|
40
|
+
<TouchableOpacity
|
|
41
|
+
style={[
|
|
42
|
+
styles.optionRow,
|
|
43
|
+
{
|
|
44
|
+
borderBottomWidth: showInput ? 1 : 0,
|
|
45
|
+
borderBottomColor: tokens.colors.border,
|
|
46
|
+
paddingVertical: tokens.spacing.md,
|
|
47
|
+
paddingHorizontal: tokens.spacing.md,
|
|
48
|
+
},
|
|
49
|
+
]}
|
|
50
|
+
onPress={onSelect}
|
|
51
|
+
activeOpacity={0.7}
|
|
52
|
+
>
|
|
53
|
+
<AtomicText
|
|
54
|
+
type="bodyMedium"
|
|
55
|
+
style={[
|
|
56
|
+
styles.optionText,
|
|
57
|
+
isSelected && { color: tokens.colors.primary, fontWeight: "600" },
|
|
58
|
+
]}
|
|
59
|
+
>
|
|
60
|
+
{text}
|
|
61
|
+
</AtomicText>
|
|
62
|
+
|
|
63
|
+
<View
|
|
64
|
+
style={[
|
|
65
|
+
styles.radioButton,
|
|
66
|
+
{
|
|
67
|
+
borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
|
|
68
|
+
backgroundColor: isSelected ? tokens.colors.primary : "transparent",
|
|
69
|
+
},
|
|
70
|
+
]}
|
|
71
|
+
>
|
|
72
|
+
{isSelected && (
|
|
73
|
+
<View style={[styles.radioButtonInner, { backgroundColor: tokens.colors.primary }]} />
|
|
74
|
+
)}
|
|
75
|
+
</View>
|
|
76
|
+
</TouchableOpacity>
|
|
77
|
+
|
|
78
|
+
{showInput && (
|
|
79
|
+
<View style={styles.inputContainer}>
|
|
80
|
+
<TextInput
|
|
81
|
+
style={[
|
|
82
|
+
styles.textInput,
|
|
83
|
+
{
|
|
84
|
+
backgroundColor: tokens.colors.surface,
|
|
85
|
+
borderRadius: tokens.borderRadius.sm,
|
|
86
|
+
padding: tokens.spacing.sm,
|
|
87
|
+
color: tokens.colors.textPrimary,
|
|
88
|
+
minHeight: 80,
|
|
89
|
+
textAlignVertical: "top",
|
|
90
|
+
},
|
|
91
|
+
]}
|
|
92
|
+
placeholder={placeholder}
|
|
93
|
+
placeholderTextColor={tokens.colors.textTertiary}
|
|
94
|
+
multiline
|
|
95
|
+
maxLength={200}
|
|
96
|
+
value={inputValue}
|
|
97
|
+
onChangeText={onChangeText}
|
|
98
|
+
autoFocus
|
|
99
|
+
/>
|
|
100
|
+
</View>
|
|
101
|
+
)}
|
|
102
|
+
</View>
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
FeedbackOption.displayName = "FeedbackOption";
|
|
107
|
+
|
|
108
|
+
const styles = StyleSheet.create({
|
|
109
|
+
optionRow: {
|
|
110
|
+
flexDirection: "row",
|
|
111
|
+
alignItems: "center",
|
|
112
|
+
justifyContent: "space-between",
|
|
113
|
+
},
|
|
114
|
+
optionText: {
|
|
115
|
+
flex: 1,
|
|
116
|
+
marginRight: 12,
|
|
117
|
+
},
|
|
118
|
+
radioButton: {
|
|
119
|
+
width: 22,
|
|
120
|
+
height: 22,
|
|
121
|
+
borderRadius: 11,
|
|
122
|
+
borderWidth: 2,
|
|
123
|
+
justifyContent: "center",
|
|
124
|
+
alignItems: "center",
|
|
125
|
+
},
|
|
126
|
+
radioButtonInner: {
|
|
127
|
+
width: 12,
|
|
128
|
+
height: 12,
|
|
129
|
+
borderRadius: 6,
|
|
130
|
+
},
|
|
131
|
+
inputContainer: {
|
|
132
|
+
padding: 12,
|
|
133
|
+
},
|
|
134
|
+
textInput: {
|
|
135
|
+
fontSize: 15,
|
|
136
|
+
borderWidth: 1,
|
|
137
|
+
borderColor: "#ccc",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React, { useMemo } from "react";
|
|
2
|
-
import { View, TouchableOpacity
|
|
2
|
+
import { View, TouchableOpacity } from "react-native";
|
|
3
3
|
import { AtomicText, BaseModal, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
4
4
|
import { usePaywallFeedback } from "../../../../../presentation/hooks/feedback/usePaywallFeedback";
|
|
5
5
|
import { createPaywallFeedbackStyles } from "./paywallFeedbackStyles";
|
|
6
|
+
import { FeedbackOption } from "./FeedbackOption";
|
|
6
7
|
|
|
7
8
|
const FEEDBACK_OPTION_IDS = [
|
|
8
9
|
"too_expensive",
|
|
@@ -69,77 +70,18 @@ export const PaywallFeedbackModal: React.FC<PaywallFeedbackModalProps> = React.m
|
|
|
69
70
|
const isSelected = selectedReason === optionId;
|
|
70
71
|
const isOther = optionId === "other";
|
|
71
72
|
const showInput = isSelected && isOther;
|
|
72
|
-
const displayText = translations.reasons[optionId];
|
|
73
|
-
|
|
74
|
-
// Dynamic styles for the container
|
|
75
|
-
const containerStyle = {
|
|
76
|
-
marginBottom: tokens.spacing.sm,
|
|
77
|
-
backgroundColor: tokens.colors.surfaceVariant,
|
|
78
|
-
borderRadius: tokens.borderRadius.md,
|
|
79
|
-
overflow: 'hidden' as const, // Ensure children don't bleed out
|
|
80
|
-
};
|
|
81
73
|
|
|
82
74
|
return (
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}}
|
|
94
|
-
onPress={() => selectReason(optionId)}
|
|
95
|
-
activeOpacity={0.7}
|
|
96
|
-
>
|
|
97
|
-
<AtomicText
|
|
98
|
-
type="bodyMedium"
|
|
99
|
-
style={[
|
|
100
|
-
styles.optionText,
|
|
101
|
-
isSelected && { color: tokens.colors.primary, fontWeight: '600' },
|
|
102
|
-
]}
|
|
103
|
-
>
|
|
104
|
-
{displayText}
|
|
105
|
-
</AtomicText>
|
|
106
|
-
|
|
107
|
-
<View
|
|
108
|
-
style={[
|
|
109
|
-
styles.radioButton,
|
|
110
|
-
isSelected && styles.radioButtonSelected,
|
|
111
|
-
]}
|
|
112
|
-
>
|
|
113
|
-
{isSelected && (
|
|
114
|
-
<View style={styles.radioButtonInner} />
|
|
115
|
-
)}
|
|
116
|
-
</View>
|
|
117
|
-
</TouchableOpacity>
|
|
118
|
-
|
|
119
|
-
{showInput && (
|
|
120
|
-
<View style={[styles.inputContainer, { backgroundColor: 'transparent', marginTop: 0, padding: tokens.spacing.sm }]}>
|
|
121
|
-
<TextInput
|
|
122
|
-
style={[
|
|
123
|
-
styles.textInput,
|
|
124
|
-
{
|
|
125
|
-
minHeight: 80,
|
|
126
|
-
textAlignVertical: 'top',
|
|
127
|
-
backgroundColor: tokens.colors.surface, // Slightly different background for input
|
|
128
|
-
borderRadius: tokens.borderRadius.sm,
|
|
129
|
-
padding: tokens.spacing.sm,
|
|
130
|
-
}
|
|
131
|
-
]}
|
|
132
|
-
placeholder={translations.otherPlaceholder}
|
|
133
|
-
placeholderTextColor={tokens.colors.textTertiary}
|
|
134
|
-
multiline
|
|
135
|
-
maxLength={200}
|
|
136
|
-
value={otherText}
|
|
137
|
-
onChangeText={setOtherText}
|
|
138
|
-
autoFocus
|
|
139
|
-
/>
|
|
140
|
-
</View>
|
|
141
|
-
)}
|
|
142
|
-
</View>
|
|
75
|
+
<FeedbackOption
|
|
76
|
+
key={optionId}
|
|
77
|
+
isSelected={isSelected}
|
|
78
|
+
text={translations.reasons[optionId]}
|
|
79
|
+
showInput={showInput}
|
|
80
|
+
placeholder={translations.otherPlaceholder}
|
|
81
|
+
inputValue={otherText}
|
|
82
|
+
onSelect={() => selectReason(optionId)}
|
|
83
|
+
onChangeText={setOtherText}
|
|
84
|
+
/>
|
|
143
85
|
);
|
|
144
86
|
})}
|
|
145
87
|
</View>
|
|
@@ -159,4 +101,7 @@ export const PaywallFeedbackModal: React.FC<PaywallFeedbackModalProps> = React.m
|
|
|
159
101
|
);
|
|
160
102
|
});
|
|
161
103
|
|
|
104
|
+
PaywallFeedbackModal.displayName = "PaywallFeedbackModal";
|
|
105
|
+
|
|
106
|
+
|
|
162
107
|
|
|
@@ -10,105 +10,13 @@ export const createPaywallFeedbackStyles = (
|
|
|
10
10
|
tokens: DesignTokens,
|
|
11
11
|
canSubmit: boolean
|
|
12
12
|
) => {
|
|
13
|
-
const overlayColor = tokens.colors.backgroundPrimary + '99';
|
|
14
|
-
|
|
15
13
|
return StyleSheet.create({
|
|
16
|
-
overlay: {
|
|
17
|
-
flex: 1,
|
|
18
|
-
backgroundColor: overlayColor,
|
|
19
|
-
justifyContent: "center",
|
|
20
|
-
alignItems: "center",
|
|
21
|
-
padding: 20,
|
|
22
|
-
},
|
|
23
|
-
keyboardView: {
|
|
24
|
-
width: "100%",
|
|
25
|
-
alignItems: "center",
|
|
26
|
-
},
|
|
27
|
-
container: {
|
|
28
|
-
width: "100%",
|
|
29
|
-
maxWidth: 360,
|
|
30
|
-
backgroundColor: tokens.colors.surface,
|
|
31
|
-
borderRadius: 24,
|
|
32
|
-
padding: 24,
|
|
33
|
-
borderWidth: 1,
|
|
34
|
-
borderColor: tokens.colors.border,
|
|
35
|
-
},
|
|
36
|
-
header: {
|
|
37
|
-
alignItems: "center",
|
|
38
|
-
marginBottom: 20,
|
|
39
|
-
},
|
|
40
|
-
title: {
|
|
41
|
-
color: tokens.colors.textPrimary,
|
|
42
|
-
marginBottom: 8,
|
|
43
|
-
textAlign: "center",
|
|
44
|
-
},
|
|
45
|
-
subtitle: {
|
|
46
|
-
color: tokens.colors.textSecondary,
|
|
47
|
-
textAlign: "center",
|
|
48
|
-
lineHeight: 22,
|
|
49
|
-
},
|
|
50
14
|
optionsContainer: {
|
|
51
15
|
backgroundColor: tokens.colors.surfaceSecondary,
|
|
52
16
|
borderRadius: 16,
|
|
53
17
|
overflow: "hidden",
|
|
54
18
|
marginBottom: 20,
|
|
55
19
|
},
|
|
56
|
-
optionRow: {
|
|
57
|
-
flexDirection: "row",
|
|
58
|
-
alignItems: "center",
|
|
59
|
-
justifyContent: "space-between",
|
|
60
|
-
paddingVertical: 16,
|
|
61
|
-
paddingHorizontal: 16,
|
|
62
|
-
borderBottomWidth: 1,
|
|
63
|
-
borderBottomColor: tokens.colors.border,
|
|
64
|
-
},
|
|
65
|
-
optionRowLast: {
|
|
66
|
-
borderBottomWidth: 0,
|
|
67
|
-
},
|
|
68
|
-
optionText: {
|
|
69
|
-
color: tokens.colors.textSecondary,
|
|
70
|
-
flex: 1,
|
|
71
|
-
marginRight: 12,
|
|
72
|
-
},
|
|
73
|
-
optionTextSelected: {
|
|
74
|
-
color: tokens.colors.textPrimary,
|
|
75
|
-
fontWeight: "600",
|
|
76
|
-
},
|
|
77
|
-
radioButton: {
|
|
78
|
-
width: 22,
|
|
79
|
-
height: 22,
|
|
80
|
-
borderRadius: 11,
|
|
81
|
-
borderWidth: 2,
|
|
82
|
-
borderColor: tokens.colors.border,
|
|
83
|
-
justifyContent: "center",
|
|
84
|
-
alignItems: "center",
|
|
85
|
-
backgroundColor: "transparent",
|
|
86
|
-
},
|
|
87
|
-
radioButtonSelected: {
|
|
88
|
-
borderColor: tokens.colors.primary,
|
|
89
|
-
borderWidth: 2,
|
|
90
|
-
},
|
|
91
|
-
radioButtonInner: {
|
|
92
|
-
width: 12,
|
|
93
|
-
height: 12,
|
|
94
|
-
borderRadius: 6,
|
|
95
|
-
backgroundColor: tokens.colors.primary,
|
|
96
|
-
},
|
|
97
|
-
inputContainer: {
|
|
98
|
-
paddingHorizontal: 16,
|
|
99
|
-
paddingBottom: 16,
|
|
100
|
-
},
|
|
101
|
-
textInput: {
|
|
102
|
-
backgroundColor: tokens.colors.surface,
|
|
103
|
-
borderRadius: 12,
|
|
104
|
-
borderWidth: 1,
|
|
105
|
-
borderColor: tokens.colors.border,
|
|
106
|
-
padding: 12,
|
|
107
|
-
fontSize: 15,
|
|
108
|
-
color: tokens.colors.textPrimary,
|
|
109
|
-
minHeight: 80,
|
|
110
|
-
textAlignVertical: "top",
|
|
111
|
-
},
|
|
112
20
|
submitButton: {
|
|
113
21
|
backgroundColor: canSubmit ? tokens.colors.primary : tokens.colors.surfaceSecondary,
|
|
114
22
|
borderRadius: 14,
|
|
@@ -32,21 +32,10 @@ const initialState: PurchaseLoadingState = {
|
|
|
32
32
|
purchaseSource: null,
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set
|
|
35
|
+
export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set) => ({
|
|
36
36
|
...initialState,
|
|
37
37
|
|
|
38
38
|
startPurchase: (productId, source) => {
|
|
39
|
-
const currentState = get();
|
|
40
|
-
if (currentState.isPurchasing) {
|
|
41
|
-
if (__DEV__) {
|
|
42
|
-
console.warn("[PurchaseLoadingStore] startPurchase called while purchase already in progress:", {
|
|
43
|
-
currentProductId: currentState.purchasingProductId,
|
|
44
|
-
newProductId: productId,
|
|
45
|
-
currentSource: currentState.purchaseSource,
|
|
46
|
-
newSource: source,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
39
|
set({
|
|
51
40
|
isPurchasing: true,
|
|
52
41
|
purchasingProductId: productId,
|
|
@@ -55,12 +44,6 @@ export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set, get) =
|
|
|
55
44
|
},
|
|
56
45
|
|
|
57
46
|
endPurchase: () => {
|
|
58
|
-
const currentState = get();
|
|
59
|
-
if (!currentState.isPurchasing) {
|
|
60
|
-
if (__DEV__) {
|
|
61
|
-
console.warn("[PurchaseLoadingStore] endPurchase called while no purchase in progress");
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
47
|
set({
|
|
65
48
|
isPurchasing: false,
|
|
66
49
|
purchasingProductId: null,
|
|
@@ -7,59 +7,24 @@ import { useCallback } from "react";
|
|
|
7
7
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
8
8
|
import { usePremium } from "./usePremium";
|
|
9
9
|
import type { PurchaseSource } from "../core/SubscriptionConstants";
|
|
10
|
+
import { authPurchaseStateManager } from "../infrastructure/utils/authPurchaseState";
|
|
10
11
|
|
|
11
|
-
export
|
|
12
|
-
isAuthenticated: () => boolean;
|
|
13
|
-
showAuthModal: () => void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let globalAuthProvider: PurchaseAuthProvider | null = null;
|
|
17
|
-
|
|
18
|
-
interface SavedPurchaseState {
|
|
19
|
-
pkg: PurchasesPackage;
|
|
20
|
-
source: PurchaseSource;
|
|
21
|
-
timestamp: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const SAVED_PURCHASE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
|
|
25
|
-
let savedPurchaseState: SavedPurchaseState | null = null;
|
|
12
|
+
export type { PurchaseAuthProvider } from "../infrastructure/utils/authPurchaseState";
|
|
26
13
|
|
|
27
|
-
export const configureAuthProvider = (provider: PurchaseAuthProvider): void => {
|
|
28
|
-
|
|
14
|
+
export const configureAuthProvider = (provider: import("../infrastructure/utils/authPurchaseState").PurchaseAuthProvider): void => {
|
|
15
|
+
authPurchaseStateManager.configure(provider);
|
|
29
16
|
};
|
|
30
17
|
|
|
31
|
-
/**
|
|
32
|
-
* Cleanup method to reset global auth provider state
|
|
33
|
-
* Call this when app is shutting down or auth system is being reset
|
|
34
|
-
*/
|
|
35
18
|
export const cleanupAuthProvider = (): void => {
|
|
36
|
-
|
|
37
|
-
console.log("[useAuthAwarePurchase] Cleaning up auth provider");
|
|
38
|
-
}
|
|
39
|
-
globalAuthProvider = null;
|
|
40
|
-
clearSavedPurchase();
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const savePurchase = (pkg: PurchasesPackage, source: PurchaseSource): void => {
|
|
44
|
-
savedPurchaseState = { pkg, source, timestamp: Date.now() };
|
|
19
|
+
authPurchaseStateManager.cleanup();
|
|
45
20
|
};
|
|
46
21
|
|
|
47
22
|
export const getSavedPurchase = (): { pkg: PurchasesPackage; source: PurchaseSource } | null => {
|
|
48
|
-
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const isExpired = Date.now() - savedPurchaseState.timestamp > SAVED_PURCHASE_EXPIRY_MS;
|
|
53
|
-
if (isExpired) {
|
|
54
|
-
savedPurchaseState = null;
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return { pkg: savedPurchaseState.pkg, source: savedPurchaseState.source };
|
|
23
|
+
return authPurchaseStateManager.getSavedPurchase();
|
|
59
24
|
};
|
|
60
25
|
|
|
61
26
|
export const clearSavedPurchase = (): void => {
|
|
62
|
-
|
|
27
|
+
authPurchaseStateManager.clearSavedPurchase();
|
|
63
28
|
};
|
|
64
29
|
|
|
65
30
|
export interface UseAuthAwarePurchaseParams {
|
|
@@ -79,25 +44,17 @@ export const useAuthAwarePurchase = (
|
|
|
79
44
|
|
|
80
45
|
const handlePurchase = useCallback(
|
|
81
46
|
async (pkg: PurchasesPackage, source?: PurchaseSource): Promise<boolean> => {
|
|
82
|
-
|
|
83
|
-
console.log("[useAuthAwarePurchase] handlePurchase:", {
|
|
84
|
-
productId: pkg.product.identifier,
|
|
85
|
-
hasAuthProvider: !!globalAuthProvider,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
47
|
+
const authProvider = authPurchaseStateManager.getProvider();
|
|
88
48
|
|
|
89
|
-
if (!
|
|
90
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
91
|
-
console.error("[useAuthAwarePurchase] Auth provider not configured");
|
|
92
|
-
}
|
|
49
|
+
if (!authProvider) {
|
|
93
50
|
return false;
|
|
94
51
|
}
|
|
95
52
|
|
|
96
|
-
const isAuth =
|
|
53
|
+
const isAuth = authProvider.isAuthenticated();
|
|
97
54
|
|
|
98
55
|
if (!isAuth) {
|
|
99
|
-
savePurchase(pkg, source || params?.source || "settings");
|
|
100
|
-
|
|
56
|
+
authPurchaseStateManager.savePurchase(pkg, source || params?.source || "settings");
|
|
57
|
+
authProvider.showAuthModal();
|
|
101
58
|
return false;
|
|
102
59
|
}
|
|
103
60
|
|
|
@@ -107,12 +64,14 @@ export const useAuthAwarePurchase = (
|
|
|
107
64
|
);
|
|
108
65
|
|
|
109
66
|
const handleRestore = useCallback(async (): Promise<boolean> => {
|
|
110
|
-
|
|
67
|
+
const authProvider = authPurchaseStateManager.getProvider();
|
|
68
|
+
|
|
69
|
+
if (!authProvider) {
|
|
111
70
|
return false;
|
|
112
71
|
}
|
|
113
72
|
|
|
114
|
-
if (!
|
|
115
|
-
|
|
73
|
+
if (!authProvider.isAuthenticated()) {
|
|
74
|
+
authProvider.showAuthModal();
|
|
116
75
|
return false;
|
|
117
76
|
}
|
|
118
77
|
|
|
@@ -120,29 +79,16 @@ export const useAuthAwarePurchase = (
|
|
|
120
79
|
}, [restorePurchase]);
|
|
121
80
|
|
|
122
81
|
const executeSavedPurchase = useCallback(async (): Promise<boolean> => {
|
|
123
|
-
const saved = getSavedPurchase();
|
|
82
|
+
const saved = authPurchaseStateManager.getSavedPurchase();
|
|
124
83
|
if (!saved) {
|
|
125
84
|
return false;
|
|
126
85
|
}
|
|
127
86
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
const result = await purchasePackage(saved.pkg);
|
|
134
|
-
// Only clear after successful purchase
|
|
135
|
-
if (result) {
|
|
136
|
-
clearSavedPurchase();
|
|
137
|
-
}
|
|
138
|
-
return result;
|
|
139
|
-
} catch (error) {
|
|
140
|
-
// Don't clear on error - allow retry
|
|
141
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
142
|
-
console.error("[useAuthAwarePurchase] Saved purchase failed:", error);
|
|
143
|
-
}
|
|
144
|
-
throw error;
|
|
87
|
+
const result = await purchasePackage(saved.pkg);
|
|
88
|
+
if (result) {
|
|
89
|
+
authPurchaseStateManager.clearSavedPurchase();
|
|
145
90
|
}
|
|
91
|
+
return result;
|
|
146
92
|
}, [purchasePackage]);
|
|
147
93
|
|
|
148
94
|
return {
|
|
@@ -47,16 +47,10 @@ export const usePremium = (): UsePremiumResult => {
|
|
|
47
47
|
|
|
48
48
|
const handlePurchase = useCallback(
|
|
49
49
|
async (pkg: PurchasesPackage): Promise<boolean> => {
|
|
50
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
51
|
-
console.log("[usePremium] handlePurchase:", pkg.product.identifier);
|
|
52
|
-
}
|
|
53
50
|
try {
|
|
54
51
|
const result = await purchaseMutation.mutateAsync(pkg);
|
|
55
52
|
return result.success;
|
|
56
|
-
} catch
|
|
57
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
58
|
-
console.error("[usePremium] Purchase failed:", error);
|
|
59
|
-
}
|
|
53
|
+
} catch {
|
|
60
54
|
return false;
|
|
61
55
|
}
|
|
62
56
|
},
|
|
@@ -67,10 +61,7 @@ export const usePremium = (): UsePremiumResult => {
|
|
|
67
61
|
try {
|
|
68
62
|
const result = await restoreMutation.mutateAsync();
|
|
69
63
|
return result.success;
|
|
70
|
-
} catch
|
|
71
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
72
|
-
console.error("[usePremium] Restore failed:", error);
|
|
73
|
-
}
|
|
64
|
+
} catch {
|
|
74
65
|
return false;
|
|
75
66
|
}
|
|
76
67
|
}, [restoreMutation]);
|
|
@@ -37,13 +37,8 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
|
37
37
|
|
|
38
38
|
try {
|
|
39
39
|
const result = await SubscriptionManager.checkPremiumStatus();
|
|
40
|
-
// Ensure we always return a valid object even if result is null/undefined
|
|
41
40
|
return result ?? { isPremium: false, expirationDate: null };
|
|
42
|
-
} catch
|
|
43
|
-
if (__DEV__) {
|
|
44
|
-
console.error('[useSubscriptionStatus] Failed to check premium status:', error);
|
|
45
|
-
}
|
|
46
|
-
// Return default state on error to prevent crashes
|
|
41
|
+
} catch {
|
|
47
42
|
return { isPremium: false, expirationDate: null };
|
|
48
43
|
}
|
|
49
44
|
},
|
|
@@ -18,8 +18,7 @@ export async function checkTrialEligibility(userId?: string, deviceId?: string):
|
|
|
18
18
|
const id = deviceId || await getDeviceId();
|
|
19
19
|
const record = await repository.getRecord(id);
|
|
20
20
|
return TrialEligibilityService.check(userId, id, record);
|
|
21
|
-
} catch
|
|
22
|
-
if (__DEV__) console.error("[TrialService] Eligibility check error:", error);
|
|
21
|
+
} catch {
|
|
23
22
|
return { eligible: false, reason: "error" };
|
|
24
23
|
}
|
|
25
24
|
}
|
|
@@ -34,8 +33,7 @@ export async function recordTrialStart(userId: string, deviceId?: string): Promi
|
|
|
34
33
|
lastUserId: userId,
|
|
35
34
|
userIds: arrayUnion(userId) as any,
|
|
36
35
|
});
|
|
37
|
-
} catch
|
|
38
|
-
if (__DEV__) console.error("[TrialService] Record trial error:", error);
|
|
36
|
+
} catch {
|
|
39
37
|
return false;
|
|
40
38
|
}
|
|
41
39
|
}
|
|
@@ -48,8 +46,7 @@ export async function recordTrialEnd(deviceId?: string): Promise<boolean> {
|
|
|
48
46
|
trialInProgress: false,
|
|
49
47
|
trialEndedAt: serverTimestamp() as any,
|
|
50
48
|
});
|
|
51
|
-
} catch
|
|
52
|
-
if (__DEV__) console.error("[TrialService] Record trial end error:", error);
|
|
49
|
+
} catch {
|
|
53
50
|
return false;
|
|
54
51
|
}
|
|
55
52
|
}
|
|
@@ -62,8 +59,7 @@ export async function recordTrialConversion(deviceId?: string): Promise<boolean>
|
|
|
62
59
|
trialInProgress: false,
|
|
63
60
|
trialConvertedAt: serverTimestamp() as any,
|
|
64
61
|
});
|
|
65
|
-
} catch
|
|
66
|
-
if (__DEV__) console.error("[TrialService] Record conversion error:", error);
|
|
62
|
+
} catch {
|
|
67
63
|
return false;
|
|
68
64
|
}
|
|
69
65
|
}
|
|
@@ -58,21 +58,8 @@ export class ProductMetadataService {
|
|
|
58
58
|
try {
|
|
59
59
|
const data = await this.fetchFromFirebase();
|
|
60
60
|
this.cache = { data, timestamp: Date.now() };
|
|
61
|
-
|
|
62
|
-
if (__DEV__) {
|
|
63
|
-
console.log(
|
|
64
|
-
"[ProductMetadataService] Loaded:",
|
|
65
|
-
data.length,
|
|
66
|
-
"products"
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
61
|
return data;
|
|
71
62
|
} catch (error) {
|
|
72
|
-
if (__DEV__) {
|
|
73
|
-
console.error("[ProductMetadataService] Fetch error:", error);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
63
|
if (this.cache) {
|
|
77
64
|
return this.cache.data;
|
|
78
65
|
}
|
|
@@ -72,16 +72,6 @@ export function useProductMetadata({
|
|
|
72
72
|
const creditsPackages = products.filter((p) => p.type === "credits");
|
|
73
73
|
const subscriptionPackages = products.filter((p) => p.type === "subscription");
|
|
74
74
|
|
|
75
|
-
if (__DEV__) {
|
|
76
|
-
console.log("[useProductMetadata] State", {
|
|
77
|
-
enabled,
|
|
78
|
-
isLoading,
|
|
79
|
-
count: products.length,
|
|
80
|
-
credits: creditsPackages.length,
|
|
81
|
-
subscriptions: subscriptionPackages.length,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
75
|
return {
|
|
86
76
|
products,
|
|
87
77
|
isLoading,
|
|
@@ -69,14 +69,6 @@ export function useTransactionHistory({
|
|
|
69
69
|
|
|
70
70
|
const transactions = data ?? [];
|
|
71
71
|
|
|
72
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
73
|
-
console.log("[useTransactionHistory] State", {
|
|
74
|
-
userId: userId?.slice(0, 8),
|
|
75
|
-
isLoading,
|
|
76
|
-
count: transactions.length,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
72
|
return {
|
|
81
73
|
transactions,
|
|
82
74
|
isLoading,
|