@umituz/react-native-subscription 2.14.55 → 2.14.57
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 +37 -12
- package/src/domains/paywall/components/PaywallContainer.types.ts +7 -2
- package/src/domains/paywall/components/PaywallModal.tsx +179 -208
- package/src/index.ts +8 -0
- package/src/presentation/hooks/useCredits.ts +0 -2
- package/src/presentation/hooks/useSubscriptionStatus.ts +0 -2
- package/src/revenuecat/infrastructure/services/RevenueCatService.ts +18 -22
- package/src/utils/packageFilter.ts +94 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.57",
|
|
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",
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PaywallContainer Component
|
|
3
|
-
* Package-driven paywall
|
|
4
|
-
*
|
|
3
|
+
* Package-driven paywall with mode-based filtering
|
|
4
|
+
* Mode: credits | subscription | hybrid
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, { useCallback } from "react";
|
|
7
|
+
import React, { useCallback, useEffect, useMemo } from "react";
|
|
8
8
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
9
9
|
import { usePaywallVisibility } from "../../../presentation/hooks/usePaywallVisibility";
|
|
10
10
|
import { useSubscriptionPackages } from "../../../revenuecat/presentation/hooks/useSubscriptionPackages";
|
|
11
11
|
import { usePurchasePackage } from "../../../revenuecat/presentation/hooks/usePurchasePackage";
|
|
12
12
|
import { useRestorePurchase } from "../../../revenuecat/presentation/hooks/useRestorePurchase";
|
|
13
|
+
import { SubscriptionManager } from "../../../revenuecat/infrastructure/managers/SubscriptionManager";
|
|
14
|
+
import { filterPackagesByMode } from "../../../utils/packageFilter";
|
|
13
15
|
import { PaywallModal } from "./PaywallModal";
|
|
14
16
|
import type { PaywallContainerProps } from "./PaywallContainer.types";
|
|
15
17
|
|
|
@@ -24,14 +26,37 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = ({
|
|
|
24
26
|
heroImage,
|
|
25
27
|
bestValueIdentifier,
|
|
26
28
|
creditsLabel,
|
|
29
|
+
creditAmounts,
|
|
30
|
+
packageFilterConfig,
|
|
27
31
|
onPurchaseSuccess,
|
|
28
32
|
onPurchaseError,
|
|
29
33
|
}) => {
|
|
30
34
|
const { showPaywall, closePaywall } = usePaywallVisibility();
|
|
31
|
-
const { data:
|
|
35
|
+
const { data: allPackages = [], isLoading, isFetching, status, error } = useSubscriptionPackages(userId ?? undefined);
|
|
32
36
|
const { mutateAsync: purchasePackage } = usePurchasePackage(userId ?? undefined);
|
|
33
37
|
const { mutateAsync: restorePurchases } = useRestorePurchase(userId ?? undefined);
|
|
34
38
|
|
|
39
|
+
const filteredPackages = useMemo(() => {
|
|
40
|
+
return filterPackagesByMode(allPackages, mode, packageFilterConfig);
|
|
41
|
+
}, [allPackages, mode, packageFilterConfig]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (__DEV__ && showPaywall) {
|
|
45
|
+
console.log("[PaywallContainer] Paywall opened:", {
|
|
46
|
+
userId,
|
|
47
|
+
mode,
|
|
48
|
+
isConfigured: SubscriptionManager.isConfigured(),
|
|
49
|
+
isInitialized: SubscriptionManager.isInitialized(),
|
|
50
|
+
allPackagesCount: allPackages.length,
|
|
51
|
+
filteredPackagesCount: filteredPackages.length,
|
|
52
|
+
isLoading,
|
|
53
|
+
isFetching,
|
|
54
|
+
status,
|
|
55
|
+
error: error?.message ?? null,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}, [showPaywall, userId, mode, allPackages.length, filteredPackages.length, isLoading, isFetching, status, error]);
|
|
59
|
+
|
|
35
60
|
const handlePurchase = useCallback(
|
|
36
61
|
async (pkg: PurchasesPackage) => {
|
|
37
62
|
try {
|
|
@@ -46,15 +71,15 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = ({
|
|
|
46
71
|
onPurchaseSuccess?.();
|
|
47
72
|
closePaywall();
|
|
48
73
|
}
|
|
49
|
-
} catch (
|
|
50
|
-
const message =
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
51
76
|
if (__DEV__) {
|
|
52
77
|
console.error("[PaywallContainer] Purchase failed:", message);
|
|
53
78
|
}
|
|
54
79
|
onPurchaseError?.(message);
|
|
55
80
|
}
|
|
56
81
|
},
|
|
57
|
-
[purchasePackage, closePaywall, onPurchaseSuccess, onPurchaseError]
|
|
82
|
+
[purchasePackage, closePaywall, onPurchaseSuccess, onPurchaseError]
|
|
58
83
|
);
|
|
59
84
|
|
|
60
85
|
const handleRestore = useCallback(async () => {
|
|
@@ -70,8 +95,8 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = ({
|
|
|
70
95
|
onPurchaseSuccess?.();
|
|
71
96
|
closePaywall();
|
|
72
97
|
}
|
|
73
|
-
} catch (
|
|
74
|
-
const message =
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
75
100
|
if (__DEV__) {
|
|
76
101
|
console.error("[PaywallContainer] Restore failed:", message);
|
|
77
102
|
}
|
|
@@ -87,16 +112,16 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = ({
|
|
|
87
112
|
<PaywallModal
|
|
88
113
|
visible={showPaywall}
|
|
89
114
|
onClose={closePaywall}
|
|
90
|
-
mode={mode}
|
|
91
115
|
translations={translations}
|
|
92
|
-
|
|
116
|
+
packages={filteredPackages}
|
|
93
117
|
isLoading={isLoading}
|
|
94
118
|
legalUrls={legalUrls}
|
|
95
119
|
features={features ? [...features] : undefined}
|
|
96
120
|
heroImage={heroImage}
|
|
97
121
|
bestValueIdentifier={bestValueIdentifier}
|
|
98
122
|
creditsLabel={creditsLabel}
|
|
99
|
-
|
|
123
|
+
creditAmounts={creditAmounts}
|
|
124
|
+
onPurchase={handlePurchase}
|
|
100
125
|
onRestore={handleRestore}
|
|
101
126
|
/>
|
|
102
127
|
);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PaywallContainer Types
|
|
3
|
-
*
|
|
3
|
+
* Props for package-driven paywall with mode-based filtering
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { PaywallMode, PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from "../entities";
|
|
7
6
|
import type { ImageSourcePropType } from "react-native";
|
|
7
|
+
import type { PaywallMode, PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from "../entities";
|
|
8
|
+
import type { PackageFilterConfig } from "../../../utils/packageFilter";
|
|
8
9
|
|
|
9
10
|
export interface PaywallContainerProps {
|
|
10
11
|
/** User ID for subscription management */
|
|
@@ -23,6 +24,10 @@ export interface PaywallContainerProps {
|
|
|
23
24
|
readonly bestValueIdentifier?: string;
|
|
24
25
|
/** Credits label text */
|
|
25
26
|
readonly creditsLabel?: string;
|
|
27
|
+
/** Credit amounts per package identifier */
|
|
28
|
+
readonly creditAmounts?: Record<string, number>;
|
|
29
|
+
/** Custom filter config for package categorization */
|
|
30
|
+
readonly packageFilterConfig?: PackageFilterConfig;
|
|
26
31
|
/** Callback when purchase succeeds */
|
|
27
32
|
readonly onPurchaseSuccess?: () => void;
|
|
28
33
|
/** Callback when purchase fails */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Paywall Modal
|
|
3
|
-
*
|
|
3
|
+
* Renders packages passed from PaywallContainer
|
|
4
|
+
* Filtering is handled by PaywallContainer based on mode
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import React, { useState, useCallback } from "react";
|
|
@@ -9,221 +10,191 @@ import { BaseModal, useAppDesignTokens, AtomicText, AtomicIcon, AtomicSpinner }
|
|
|
9
10
|
import { Image } from "expo-image";
|
|
10
11
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
11
12
|
import { PlanCard } from "./PlanCard";
|
|
12
|
-
import {
|
|
13
|
-
import type { PaywallMode, CreditsPackage, SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from '../entities';
|
|
13
|
+
import type { SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from "../entities";
|
|
14
14
|
import { paywallModalStyles as styles } from "./PaywallModal.styles";
|
|
15
15
|
|
|
16
16
|
export interface PaywallModalProps {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
heroImage?: ImageSourcePropType;
|
|
31
|
-
onSubscriptionPurchase?: (pkg: PurchasesPackage) => Promise<void | boolean>;
|
|
32
|
-
onCreditsPurchase?: (packageId: string) => Promise<void | boolean>;
|
|
33
|
-
onRestore?: () => Promise<void | boolean>;
|
|
17
|
+
visible: boolean;
|
|
18
|
+
onClose: () => void;
|
|
19
|
+
translations: PaywallTranslations;
|
|
20
|
+
packages?: PurchasesPackage[];
|
|
21
|
+
features?: SubscriptionFeature[];
|
|
22
|
+
isLoading?: boolean;
|
|
23
|
+
legalUrls?: PaywallLegalUrls;
|
|
24
|
+
bestValueIdentifier?: string;
|
|
25
|
+
creditAmounts?: Record<string, number>;
|
|
26
|
+
creditsLabel?: string;
|
|
27
|
+
heroImage?: ImageSourcePropType;
|
|
28
|
+
onPurchase?: (pkg: PurchasesPackage) => Promise<void | boolean>;
|
|
29
|
+
onRestore?: () => Promise<void | boolean>;
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
33
|
+
const {
|
|
34
|
+
visible,
|
|
35
|
+
onClose,
|
|
36
|
+
translations,
|
|
37
|
+
packages = [],
|
|
38
|
+
features = [],
|
|
39
|
+
isLoading = false,
|
|
40
|
+
legalUrls = {},
|
|
41
|
+
bestValueIdentifier,
|
|
42
|
+
creditAmounts,
|
|
43
|
+
creditsLabel,
|
|
44
|
+
heroImage,
|
|
45
|
+
onPurchase,
|
|
46
|
+
onRestore,
|
|
47
|
+
} = props;
|
|
48
|
+
|
|
49
|
+
const tokens = useAppDesignTokens();
|
|
50
|
+
const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
|
|
51
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
52
|
+
|
|
53
|
+
const handlePurchase = useCallback(async () => {
|
|
54
|
+
if (!selectedPlanId || !onPurchase) return;
|
|
55
|
+
setIsProcessing(true);
|
|
56
|
+
try {
|
|
57
|
+
const pkg = packages.find((p) => p.product.identifier === selectedPlanId);
|
|
58
|
+
if (pkg) await onPurchase(pkg);
|
|
59
|
+
} finally {
|
|
60
|
+
setIsProcessing(false);
|
|
61
|
+
}
|
|
62
|
+
}, [selectedPlanId, packages, onPurchase]);
|
|
63
|
+
|
|
64
|
+
const handleRestore = useCallback(async () => {
|
|
65
|
+
if (!onRestore || isProcessing) return;
|
|
66
|
+
setIsProcessing(true);
|
|
67
|
+
try {
|
|
68
|
+
await onRestore();
|
|
69
|
+
} finally {
|
|
70
|
+
setIsProcessing(false);
|
|
71
|
+
}
|
|
72
|
+
}, [onRestore, isProcessing]);
|
|
73
|
+
|
|
74
|
+
const handleLegalUrl = useCallback(async (url: string | undefined) => {
|
|
75
|
+
if (!url) return;
|
|
76
|
+
try {
|
|
77
|
+
const supported = await Linking.canOpenURL(url);
|
|
78
|
+
if (supported) await Linking.openURL(url);
|
|
79
|
+
} catch {
|
|
80
|
+
// Silent fail
|
|
81
|
+
}
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const isPurchaseDisabled = !selectedPlanId;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<BaseModal visible={visible} onClose={onClose} contentStyle={styles.modalContent}>
|
|
88
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.surface }]}>
|
|
89
|
+
<TouchableOpacity
|
|
90
|
+
onPress={onClose}
|
|
91
|
+
style={[styles.closeBtn, { backgroundColor: tokens.colors.surfaceSecondary }]}
|
|
92
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
93
|
+
>
|
|
94
|
+
<AtomicIcon name="close-outline" size="md" customColor={tokens.colors.textPrimary} />
|
|
95
|
+
</TouchableOpacity>
|
|
96
|
+
|
|
97
|
+
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.scroll}>
|
|
98
|
+
{heroImage && (
|
|
99
|
+
<View style={styles.heroContainer}>
|
|
100
|
+
<Image source={heroImage} style={styles.heroImage} contentFit="cover" transition={300} />
|
|
101
|
+
</View>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
<View style={styles.header}>
|
|
105
|
+
<AtomicText type="headlineMedium" style={[styles.title, { color: tokens.colors.textPrimary }]}>
|
|
106
|
+
{translations.title}
|
|
107
|
+
</AtomicText>
|
|
108
|
+
{translations.subtitle && (
|
|
109
|
+
<AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>
|
|
110
|
+
{translations.subtitle}
|
|
111
|
+
</AtomicText>
|
|
112
|
+
)}
|
|
113
|
+
</View>
|
|
114
|
+
|
|
115
|
+
{features.length > 0 && (
|
|
116
|
+
<View style={[styles.features, { backgroundColor: tokens.colors.surfaceSecondary }]}>
|
|
117
|
+
{features.map((feature, idx) => (
|
|
118
|
+
<View key={idx} style={styles.featureRow}>
|
|
119
|
+
<View style={[styles.featureIcon, { backgroundColor: tokens.colors.primaryLight }]}>
|
|
120
|
+
<AtomicIcon name={feature.icon} customSize={16} customColor={tokens.colors.primary} />
|
|
121
|
+
</View>
|
|
122
|
+
<AtomicText type="bodyMedium" style={[styles.featureText, { color: tokens.colors.textPrimary }]}>
|
|
123
|
+
{feature.text}
|
|
124
|
+
</AtomicText>
|
|
125
|
+
</View>
|
|
126
|
+
))}
|
|
127
|
+
</View>
|
|
128
|
+
)}
|
|
112
129
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
))}
|
|
176
|
-
</View>
|
|
177
|
-
)}
|
|
178
|
-
|
|
179
|
-
<TouchableOpacity
|
|
180
|
-
onPress={handlePurchase}
|
|
181
|
-
disabled={isPurchaseDisabled || isProcessing}
|
|
182
|
-
style={[styles.cta, { backgroundColor: tokens.colors.primary }, (isPurchaseDisabled || isProcessing) && styles.ctaDisabled]}
|
|
183
|
-
activeOpacity={0.8}
|
|
184
|
-
>
|
|
185
|
-
<AtomicText type="titleLarge" style={[styles.ctaText, { color: tokens.colors.onPrimary }]}>
|
|
186
|
-
{isProcessing
|
|
187
|
-
? translations.processingText
|
|
188
|
-
: showSubscription
|
|
189
|
-
? (translations.subscribeButtonText || translations.purchaseButtonText)
|
|
190
|
-
: translations.purchaseButtonText}
|
|
191
|
-
</AtomicText>
|
|
192
|
-
</TouchableOpacity>
|
|
193
|
-
|
|
194
|
-
<View style={styles.footer}>
|
|
195
|
-
{onRestore && (
|
|
196
|
-
<TouchableOpacity
|
|
197
|
-
onPress={handleRestore}
|
|
198
|
-
disabled={isProcessing}
|
|
199
|
-
style={[styles.restoreButton, isProcessing && styles.restoreButtonDisabled]}
|
|
200
|
-
>
|
|
201
|
-
<AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.textSecondary }]}>
|
|
202
|
-
{isProcessing ? translations.processingText : translations.restoreButtonText}
|
|
203
|
-
</AtomicText>
|
|
204
|
-
</TouchableOpacity>
|
|
205
|
-
)}
|
|
206
|
-
<View style={styles.legalRow}>
|
|
207
|
-
{legalUrls.termsUrl && (
|
|
208
|
-
<TouchableOpacity onPress={() => handleLegalUrl(legalUrls.termsUrl)}>
|
|
209
|
-
<AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.textSecondary }]}>
|
|
210
|
-
{translations.termsOfServiceText}
|
|
211
|
-
</AtomicText>
|
|
212
|
-
</TouchableOpacity>
|
|
213
|
-
)}
|
|
214
|
-
{legalUrls.privacyUrl && (
|
|
215
|
-
<TouchableOpacity onPress={() => handleLegalUrl(legalUrls.privacyUrl)}>
|
|
216
|
-
<AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.textSecondary }]}>
|
|
217
|
-
{translations.privacyText}
|
|
218
|
-
</AtomicText>
|
|
219
|
-
</TouchableOpacity>
|
|
220
|
-
)}
|
|
221
|
-
</View>
|
|
222
|
-
</View>
|
|
223
|
-
</ScrollView>
|
|
130
|
+
{isLoading ? (
|
|
131
|
+
<View style={styles.loading}>
|
|
132
|
+
<AtomicSpinner size="lg" color="primary" text={translations.loadingText} />
|
|
133
|
+
</View>
|
|
134
|
+
) : (
|
|
135
|
+
<View style={styles.plans}>
|
|
136
|
+
{packages.map((pkg) => (
|
|
137
|
+
<PlanCard
|
|
138
|
+
key={pkg.product.identifier}
|
|
139
|
+
pkg={pkg}
|
|
140
|
+
isSelected={selectedPlanId === pkg.product.identifier}
|
|
141
|
+
onSelect={() => setSelectedPlanId(pkg.product.identifier)}
|
|
142
|
+
badge={pkg.product.identifier === bestValueIdentifier ? translations.bestValueBadgeText : undefined}
|
|
143
|
+
creditAmount={creditAmounts?.[pkg.product.identifier]}
|
|
144
|
+
creditsLabel={creditsLabel}
|
|
145
|
+
/>
|
|
146
|
+
))}
|
|
147
|
+
</View>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
<TouchableOpacity
|
|
151
|
+
onPress={handlePurchase}
|
|
152
|
+
disabled={isPurchaseDisabled || isProcessing}
|
|
153
|
+
style={[
|
|
154
|
+
styles.cta,
|
|
155
|
+
{ backgroundColor: tokens.colors.primary },
|
|
156
|
+
(isPurchaseDisabled || isProcessing) && styles.ctaDisabled,
|
|
157
|
+
]}
|
|
158
|
+
activeOpacity={0.8}
|
|
159
|
+
>
|
|
160
|
+
<AtomicText type="titleLarge" style={[styles.ctaText, { color: tokens.colors.onPrimary }]}>
|
|
161
|
+
{isProcessing ? translations.processingText : translations.purchaseButtonText}
|
|
162
|
+
</AtomicText>
|
|
163
|
+
</TouchableOpacity>
|
|
164
|
+
|
|
165
|
+
<View style={styles.footer}>
|
|
166
|
+
{onRestore && (
|
|
167
|
+
<TouchableOpacity
|
|
168
|
+
onPress={handleRestore}
|
|
169
|
+
disabled={isProcessing}
|
|
170
|
+
style={[styles.restoreButton, isProcessing && styles.restoreButtonDisabled]}
|
|
171
|
+
>
|
|
172
|
+
<AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.textSecondary }]}>
|
|
173
|
+
{isProcessing ? translations.processingText : translations.restoreButtonText}
|
|
174
|
+
</AtomicText>
|
|
175
|
+
</TouchableOpacity>
|
|
176
|
+
)}
|
|
177
|
+
<View style={styles.legalRow}>
|
|
178
|
+
{legalUrls.termsUrl && (
|
|
179
|
+
<TouchableOpacity onPress={() => handleLegalUrl(legalUrls.termsUrl)}>
|
|
180
|
+
<AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.textSecondary }]}>
|
|
181
|
+
{translations.termsOfServiceText}
|
|
182
|
+
</AtomicText>
|
|
183
|
+
</TouchableOpacity>
|
|
184
|
+
)}
|
|
185
|
+
{legalUrls.privacyUrl && (
|
|
186
|
+
<TouchableOpacity onPress={() => handleLegalUrl(legalUrls.privacyUrl)}>
|
|
187
|
+
<AtomicText type="bodySmall" style={[styles.footerLink, { color: tokens.colors.textSecondary }]}>
|
|
188
|
+
{translations.privacyText}
|
|
189
|
+
</AtomicText>
|
|
190
|
+
</TouchableOpacity>
|
|
191
|
+
)}
|
|
224
192
|
</View>
|
|
225
|
-
|
|
226
|
-
|
|
193
|
+
</View>
|
|
194
|
+
</ScrollView>
|
|
195
|
+
</View>
|
|
196
|
+
</BaseModal>
|
|
197
|
+
);
|
|
227
198
|
});
|
|
228
199
|
|
|
229
200
|
PaywallModal.displayName = "PaywallModal";
|
package/src/index.ts
CHANGED
|
@@ -376,6 +376,14 @@ export {
|
|
|
376
376
|
type SubscriptionPackageType,
|
|
377
377
|
} from "./utils/packageTypeDetector";
|
|
378
378
|
|
|
379
|
+
export {
|
|
380
|
+
filterPackagesByMode,
|
|
381
|
+
separatePackages,
|
|
382
|
+
getPackageCategory,
|
|
383
|
+
type PackageCategory,
|
|
384
|
+
type PackageFilterConfig,
|
|
385
|
+
} from "./utils/packageFilter";
|
|
386
|
+
|
|
379
387
|
export {
|
|
380
388
|
getCreditAllocation,
|
|
381
389
|
getImageCreditsForPackage,
|
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
import { useQuery } from "@tanstack/react-query";
|
|
9
9
|
import { useCallback, useMemo } from "react";
|
|
10
10
|
import type { UserCredits, CreditType } from "../../domain/entities/Credits";
|
|
11
|
-
|
|
12
|
-
declare const __DEV__: boolean;
|
|
13
11
|
import {
|
|
14
12
|
getCreditsRepository,
|
|
15
13
|
getCreditsConfig,
|
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
import { useQuery } from "@tanstack/react-query";
|
|
9
9
|
import { SubscriptionManager } from "../../revenuecat/infrastructure/managers/SubscriptionManager";
|
|
10
10
|
|
|
11
|
-
declare const __DEV__: boolean;
|
|
12
|
-
|
|
13
11
|
export const subscriptionStatusQueryKeys = {
|
|
14
12
|
all: ["subscriptionStatus"] as const,
|
|
15
13
|
user: (userId: string) => ["subscriptionStatus", userId] as const,
|
|
@@ -60,29 +60,25 @@ export class RevenueCatService implements IRevenueCatService {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.listenerManager.setupListener(this.stateManager.getConfig());
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return result;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
throw error;
|
|
63
|
+
const result = await initializeSDK(
|
|
64
|
+
{
|
|
65
|
+
config: this.stateManager.getConfig(),
|
|
66
|
+
isUsingTestStore: () => this.isUsingTestStore(),
|
|
67
|
+
isInitialized: () => this.isInitialized(),
|
|
68
|
+
getCurrentUserId: () => this.stateManager.getCurrentUserId(),
|
|
69
|
+
setInitialized: (value) => this.stateManager.setInitialized(value),
|
|
70
|
+
setCurrentUserId: (id) => this.stateManager.setCurrentUserId(id),
|
|
71
|
+
},
|
|
72
|
+
userId,
|
|
73
|
+
apiKey
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (result.success) {
|
|
77
|
+
this.listenerManager.setUserId(userId);
|
|
78
|
+
this.listenerManager.setupListener(this.stateManager.getConfig());
|
|
85
79
|
}
|
|
80
|
+
|
|
81
|
+
return result;
|
|
86
82
|
}
|
|
87
83
|
|
|
88
84
|
async fetchOfferings(): Promise<PurchasesOffering | null> {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Filter Utility
|
|
3
|
+
* Filters RevenueCat packages by type (credits vs subscription)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
|
+
|
|
8
|
+
declare const __DEV__: boolean;
|
|
9
|
+
|
|
10
|
+
export type PackageCategory = "credits" | "subscription";
|
|
11
|
+
|
|
12
|
+
export interface PackageFilterConfig {
|
|
13
|
+
creditIdentifierPattern?: RegExp;
|
|
14
|
+
subscriptionIdentifierPattern?: RegExp;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_CONFIG: PackageFilterConfig = {
|
|
18
|
+
creditIdentifierPattern: /credit/i,
|
|
19
|
+
subscriptionIdentifierPattern: /(monthly|yearly|annual|weekly|premium|subscription)/i,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function getPackageCategory(
|
|
23
|
+
pkg: PurchasesPackage,
|
|
24
|
+
config: PackageFilterConfig = DEFAULT_CONFIG
|
|
25
|
+
): PackageCategory {
|
|
26
|
+
const identifier = pkg.identifier.toLowerCase();
|
|
27
|
+
const productIdentifier = pkg.product.identifier.toLowerCase();
|
|
28
|
+
|
|
29
|
+
const isCreditPackage =
|
|
30
|
+
config.creditIdentifierPattern?.test(identifier) ||
|
|
31
|
+
config.creditIdentifierPattern?.test(productIdentifier);
|
|
32
|
+
|
|
33
|
+
if (isCreditPackage) {
|
|
34
|
+
if (__DEV__) {
|
|
35
|
+
console.log("[PackageFilter] Credit package:", identifier);
|
|
36
|
+
}
|
|
37
|
+
return "credits";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (__DEV__) {
|
|
41
|
+
console.log("[PackageFilter] Subscription package:", identifier);
|
|
42
|
+
}
|
|
43
|
+
return "subscription";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function filterPackagesByMode(
|
|
47
|
+
packages: PurchasesPackage[],
|
|
48
|
+
mode: "credits" | "subscription" | "hybrid",
|
|
49
|
+
config: PackageFilterConfig = DEFAULT_CONFIG
|
|
50
|
+
): PurchasesPackage[] {
|
|
51
|
+
if (mode === "hybrid") {
|
|
52
|
+
if (__DEV__) {
|
|
53
|
+
console.log("[PackageFilter] Hybrid mode - returning all packages:", packages.length);
|
|
54
|
+
}
|
|
55
|
+
return packages;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const filtered = packages.filter((pkg) => {
|
|
59
|
+
const category = getPackageCategory(pkg, config);
|
|
60
|
+
return category === mode;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (__DEV__) {
|
|
64
|
+
console.log(`[PackageFilter] Mode: ${mode}, Filtered: ${filtered.length}/${packages.length}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return filtered;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function separatePackages(
|
|
71
|
+
packages: PurchasesPackage[],
|
|
72
|
+
config: PackageFilterConfig = DEFAULT_CONFIG
|
|
73
|
+
): { creditPackages: PurchasesPackage[]; subscriptionPackages: PurchasesPackage[] } {
|
|
74
|
+
const creditPackages: PurchasesPackage[] = [];
|
|
75
|
+
const subscriptionPackages: PurchasesPackage[] = [];
|
|
76
|
+
|
|
77
|
+
for (const pkg of packages) {
|
|
78
|
+
const category = getPackageCategory(pkg, config);
|
|
79
|
+
if (category === "credits") {
|
|
80
|
+
creditPackages.push(pkg);
|
|
81
|
+
} else {
|
|
82
|
+
subscriptionPackages.push(pkg);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (__DEV__) {
|
|
87
|
+
console.log("[PackageFilter] Separated:", {
|
|
88
|
+
credits: creditPackages.length,
|
|
89
|
+
subscriptions: subscriptionPackages.length,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { creditPackages, subscriptionPackages };
|
|
94
|
+
}
|