@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.14.55",
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 that encapsulates all logic
4
- * Main app only needs to add this ONE component
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: packages = [], isLoading } = useSubscriptionPackages(userId ?? undefined);
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 (error) {
50
- const message = error instanceof Error ? error.message : String(error);
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 (error) {
74
- const message = error instanceof Error ? error.message : String(error);
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
- subscriptionPackages={packages}
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
- onSubscriptionPurchase={handlePurchase}
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
- * Minimal props for package-driven paywall rendering
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
- * Modern paywall with responsive design and theme support
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 { CreditCard } from "./CreditCard";
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
- visible: boolean;
18
- onClose: () => void;
19
- mode: PaywallMode;
20
- translations: PaywallTranslations;
21
- subscriptionPackages?: PurchasesPackage[];
22
- creditsPackages?: CreditsPackage[];
23
- features?: SubscriptionFeature[];
24
- isLoading?: boolean;
25
- legalUrls?: PaywallLegalUrls;
26
- bestValueIdentifier?: string;
27
- creditAmounts?: Record<string, number>;
28
- creditsLabel?: string;
29
- /** Hero image source (require or uri) */
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
- const {
38
- visible,
39
- onClose,
40
- mode,
41
- translations,
42
- subscriptionPackages = [],
43
- creditsPackages = [],
44
- features = [],
45
- isLoading = false,
46
- legalUrls = {},
47
- bestValueIdentifier,
48
- creditAmounts,
49
- creditsLabel,
50
- heroImage,
51
- onSubscriptionPurchase,
52
- onCreditsPurchase,
53
- onRestore,
54
- } = props;
55
-
56
- const tokens = useAppDesignTokens();
57
- const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
58
- const [selectedCreditId, setSelectedCreditId] = useState<string | null>(null);
59
- const [isProcessing, setIsProcessing] = useState(false);
60
-
61
- const showCredits = mode === "credits";
62
- const showSubscription = mode === "subscription" || mode === "hybrid";
63
-
64
- const handlePurchase = useCallback(async () => {
65
- setIsProcessing(true);
66
- try {
67
- if (showSubscription && selectedPlanId && onSubscriptionPurchase) {
68
- const pkg = subscriptionPackages.find((p) => p.product.identifier === selectedPlanId);
69
- if (pkg) await onSubscriptionPurchase(pkg);
70
- } else if (showCredits && selectedCreditId && onCreditsPurchase) {
71
- await onCreditsPurchase(selectedCreditId);
72
- }
73
- } finally {
74
- setIsProcessing(false);
75
- }
76
- }, [showSubscription, showCredits, selectedPlanId, selectedCreditId, subscriptionPackages, onSubscriptionPurchase, onCreditsPurchase]);
77
-
78
- const handleRestore = useCallback(async () => {
79
- if (!onRestore || isProcessing) return;
80
- setIsProcessing(true);
81
- try {
82
- await onRestore();
83
- } finally {
84
- setIsProcessing(false);
85
- }
86
- }, [onRestore, isProcessing]);
87
-
88
- const handleLegalUrl = useCallback(async (url: string | undefined) => {
89
- if (!url) return;
90
- try {
91
- const supported = await Linking.canOpenURL(url);
92
- if (supported) {
93
- await Linking.openURL(url);
94
- }
95
- } catch {
96
- // Silent fail
97
- }
98
- }, []);
99
-
100
- const isPurchaseDisabled = showSubscription ? !selectedPlanId : !selectedCreditId;
101
-
102
- return (
103
- <BaseModal visible={visible} onClose={onClose} contentStyle={styles.modalContent}>
104
- <View style={[styles.container, { backgroundColor: tokens.colors.surface }]}>
105
- <TouchableOpacity
106
- onPress={onClose}
107
- style={[styles.closeBtn, { backgroundColor: tokens.colors.surfaceSecondary }]}
108
- hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
109
- >
110
- <AtomicIcon name="close-outline" size="md" customColor={tokens.colors.textPrimary} />
111
- </TouchableOpacity>
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
- <ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.scroll}>
114
- {heroImage && (
115
- <View style={styles.heroContainer}>
116
- <Image
117
- source={heroImage}
118
- style={styles.heroImage}
119
- contentFit="cover"
120
- transition={300}
121
- />
122
- </View>
123
- )}
124
- <View style={styles.header}>
125
- <AtomicText type="headlineMedium" style={[styles.title, { color: tokens.colors.textPrimary }]}>
126
- {translations.title}
127
- </AtomicText>
128
- {translations.subtitle && (
129
- <AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>
130
- {translations.subtitle}
131
- </AtomicText>
132
- )}
133
- </View>
134
-
135
- {features.length > 0 && (
136
- <View style={[styles.features, { backgroundColor: tokens.colors.surfaceSecondary }]}>
137
- {features.map((feature, idx) => (
138
- <View key={idx} style={styles.featureRow}>
139
- <View style={[styles.featureIcon, { backgroundColor: tokens.colors.primaryLight }]}>
140
- <AtomicIcon name={feature.icon} customSize={16} customColor={tokens.colors.primary} />
141
- </View>
142
- <AtomicText type="bodyMedium" style={[styles.featureText, { color: tokens.colors.textPrimary }]}>
143
- {feature.text}
144
- </AtomicText>
145
- </View>
146
- ))}
147
- </View>
148
- )}
149
-
150
- {isLoading ? (
151
- <View style={styles.loading}>
152
- <AtomicSpinner
153
- size="lg"
154
- color="primary"
155
- text={translations.loadingText}
156
- />
157
- </View>
158
- ) : (
159
- <View style={styles.plans}>
160
- {showSubscription &&
161
- subscriptionPackages.map((pkg) => (
162
- <PlanCard
163
- key={pkg.product.identifier}
164
- pkg={pkg}
165
- isSelected={selectedPlanId === pkg.product.identifier}
166
- onSelect={() => setSelectedPlanId(pkg.product.identifier)}
167
- badge={pkg.product.identifier === bestValueIdentifier ? translations.bestValueBadgeText : undefined}
168
- creditAmount={creditAmounts?.[pkg.product.identifier]}
169
- creditsLabel={creditsLabel}
170
- />
171
- ))}
172
- {showCredits &&
173
- creditsPackages.map((pkg) => (
174
- <CreditCard key={pkg.id} pkg={pkg} isSelected={selectedCreditId === pkg.id} onSelect={() => setSelectedCreditId(pkg.id)} />
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
- </BaseModal>
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
- try {
64
- const result = await initializeSDK(
65
- {
66
- config: this.stateManager.getConfig(),
67
- isUsingTestStore: () => this.isUsingTestStore(),
68
- isInitialized: () => this.isInitialized(),
69
- getCurrentUserId: () => this.stateManager.getCurrentUserId(),
70
- setInitialized: (value) => this.stateManager.setInitialized(value),
71
- setCurrentUserId: (id) => this.stateManager.setCurrentUserId(id),
72
- },
73
- userId,
74
- apiKey
75
- );
76
-
77
- if (result.success) {
78
- this.listenerManager.setUserId(userId);
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
+ }