@umituz/react-native-subscription 2.33.0 → 2.33.1

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.
Files changed (51) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/infrastructure/operations/CreditsFetcher.ts +1 -2
  3. package/src/domains/credits/infrastructure/operations/CreditsInitializer.ts +1 -1
  4. package/src/domains/credits/presentation/deduct-credit/index.ts +2 -0
  5. package/src/domains/credits/presentation/deduct-credit/mutationConfig.ts +81 -0
  6. package/src/domains/credits/presentation/deduct-credit/types.ts +11 -0
  7. package/src/domains/credits/presentation/deduct-credit/useDeductCredit.ts +44 -0
  8. package/src/domains/subscription/application/initializer/BackgroundInitializer.ts +21 -0
  9. package/src/domains/subscription/application/initializer/ConfigValidator.ts +33 -0
  10. package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +45 -0
  11. package/src/domains/subscription/application/initializer/SubscriptionInitializer.ts +11 -0
  12. package/src/domains/subscription/application/initializer/index.ts +2 -0
  13. package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +13 -94
  14. package/src/domains/subscription/infrastructure/handlers/package-operations/PackageFetcher.ts +57 -0
  15. package/src/domains/subscription/infrastructure/handlers/package-operations/PackagePurchaser.ts +15 -0
  16. package/src/domains/subscription/infrastructure/handlers/package-operations/PackageRestorer.ts +34 -0
  17. package/src/domains/subscription/infrastructure/handlers/package-operations/PremiumStatusChecker.ts +9 -0
  18. package/src/domains/subscription/infrastructure/handlers/package-operations/index.ts +5 -0
  19. package/src/domains/subscription/infrastructure/handlers/package-operations/types.ts +4 -0
  20. package/src/domains/subscription/infrastructure/hooks/customer-info/index.ts +2 -0
  21. package/src/domains/subscription/infrastructure/hooks/customer-info/types.ts +9 -0
  22. package/src/domains/subscription/infrastructure/hooks/customer-info/useCustomerInfo.ts +57 -0
  23. package/src/domains/subscription/infrastructure/services/listeners/CustomerInfoHandler.ts +1 -1
  24. package/src/domains/subscription/infrastructure/services/listeners/ListenerState.ts +1 -1
  25. package/src/domains/subscription/infrastructure/services/purchase/PurchaseErrorHandler.ts +0 -1
  26. package/src/domains/subscription/infrastructure/utils/renewal/PackageTierComparator.ts +14 -0
  27. package/src/domains/subscription/infrastructure/utils/renewal/RenewalDetector.ts +78 -0
  28. package/src/domains/subscription/infrastructure/utils/renewal/RenewalStateUpdater.ts +11 -0
  29. package/src/domains/subscription/infrastructure/utils/renewal/index.ts +3 -0
  30. package/src/domains/subscription/infrastructure/utils/renewal/types.ts +14 -0
  31. package/src/domains/wallet/index.ts +2 -2
  32. package/src/domains/wallet/infrastructure/repositories/transaction/CollectionBuilder.ts +14 -0
  33. package/src/domains/wallet/infrastructure/repositories/transaction/TransactionFetcher.ts +46 -0
  34. package/src/domains/wallet/infrastructure/repositories/transaction/TransactionRepository.ts +34 -0
  35. package/src/domains/wallet/infrastructure/repositories/transaction/TransactionWriter.ts +43 -0
  36. package/src/domains/wallet/infrastructure/repositories/transaction/index.ts +10 -0
  37. package/src/domains/wallet/infrastructure/services/product-metadata/CacheManager.ts +30 -0
  38. package/src/domains/wallet/infrastructure/services/product-metadata/FirebaseFetcher.ts +17 -0
  39. package/src/domains/wallet/infrastructure/services/product-metadata/ProductMetadataService.ts +57 -0
  40. package/src/domains/wallet/infrastructure/services/product-metadata/ServiceManager.ts +29 -0
  41. package/src/domains/wallet/infrastructure/services/product-metadata/index.ts +7 -0
  42. package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +1 -1
  43. package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +1 -1
  44. package/src/index.ts +2 -2
  45. package/src/init/createSubscriptionInitModule.ts +1 -1
  46. package/src/domains/credits/presentation/useDeductCredit.ts +0 -110
  47. package/src/domains/subscription/application/SubscriptionInitializer.ts +0 -112
  48. package/src/domains/subscription/infrastructure/hooks/useCustomerInfo.ts +0 -113
  49. package/src/domains/subscription/infrastructure/utils/RenewalDetector.ts +0 -141
  50. package/src/domains/wallet/infrastructure/repositories/TransactionRepository.ts +0 -114
  51. package/src/domains/wallet/infrastructure/services/ProductMetadataService.ts +0 -114
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.33.0",
3
+ "version": "2.33.1",
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,9 +1,8 @@
1
1
  import { getDoc } from "firebase/firestore";
2
- import type { Firestore, DocumentReference } from "@umituz/react-native-firebase";
2
+ import type { DocumentReference } from "@umituz/react-native-firebase";
3
3
  import type { CreditsResult } from "../../core/Credits";
4
4
  import type { UserCreditsDocumentRead } from "../../core/UserCreditsDocument";
5
5
  import { mapCreditsDocumentToEntity } from "../../core/CreditsMapper";
6
- import { requireFirestore } from "../../../../shared/infrastructure/firestore";
7
6
 
8
7
  export async function fetchCredits(ref: DocumentReference): Promise<CreditsResult> {
9
8
  const snap = await getDoc(ref);
@@ -1,6 +1,6 @@
1
1
  import type { Firestore, DocumentReference } from "@umituz/react-native-firebase";
2
2
  import type { CreditsConfig, CreditsResult } from "../../core/Credits";
3
- import type { UserCreditsDocumentRead, PurchaseSource } from "../../core/UserCreditsDocument";
3
+ import type { PurchaseSource } from "../../core/UserCreditsDocument";
4
4
  import { initializeCreditsTransaction } from "../../application/CreditsInitializer";
5
5
  import { mapCreditsDocumentToEntity } from "../../core/CreditsMapper";
6
6
  import type { RevenueCatData } from "../../../revenuecat/core/types";
@@ -0,0 +1,2 @@
1
+ export { useDeductCredit } from "./useDeductCredit";
2
+ export type { UseDeductCreditParams, UseDeductCreditResult } from "./types";
@@ -0,0 +1,81 @@
1
+ import type { QueryClient } from "@umituz/react-native-design-system";
2
+ import { timezoneService } from "@umituz/react-native-design-system";
3
+ import type { UserCredits } from "../../core/Credits";
4
+ import type { CreditsRepository } from "../../infrastructure/CreditsRepository";
5
+ import { creditsQueryKeys } from "../creditsQueryKeys";
6
+ import { calculateRemaining } from "../../../../shared/utils/numberUtils";
7
+
8
+ interface MutationContext {
9
+ previousCredits: UserCredits | null;
10
+ skippedOptimistic: boolean;
11
+ wasInsufficient?: boolean;
12
+ capturedUserId: string | null;
13
+ }
14
+
15
+ export function createDeductCreditMutationConfig(
16
+ userId: string | undefined,
17
+ repository: CreditsRepository,
18
+ queryClient: QueryClient
19
+ ) {
20
+ return {
21
+ mutationFn: async (cost: number) => {
22
+ if (!userId) throw new Error("User not authenticated");
23
+ return repository.deductCredit(userId, cost);
24
+ },
25
+ onMutate: async (cost: number): Promise<MutationContext> => {
26
+ if (!userId) {
27
+ return { previousCredits: null, skippedOptimistic: true, capturedUserId: null };
28
+ }
29
+
30
+ const capturedUserId = userId;
31
+
32
+ await queryClient.cancelQueries({ queryKey: creditsQueryKeys.user(capturedUserId) });
33
+ const previousCredits = queryClient.getQueryData<UserCredits>(
34
+ creditsQueryKeys.user(capturedUserId)
35
+ );
36
+
37
+ if (!previousCredits) {
38
+ return {
39
+ previousCredits: null as UserCredits | null,
40
+ skippedOptimistic: true,
41
+ capturedUserId
42
+ };
43
+ }
44
+
45
+ const newCredits = calculateRemaining(previousCredits.credits, cost);
46
+
47
+ queryClient.setQueryData<UserCredits | null>(
48
+ creditsQueryKeys.user(capturedUserId),
49
+ (old) => {
50
+ if (!old) return old;
51
+ return {
52
+ ...old,
53
+ credits: newCredits,
54
+ lastUpdatedAt: timezoneService.getNow()
55
+ };
56
+ }
57
+ );
58
+
59
+ return {
60
+ previousCredits,
61
+ skippedOptimistic: false,
62
+ wasInsufficient: previousCredits.credits < cost,
63
+ capturedUserId
64
+ };
65
+ },
66
+ onError: (_err: unknown, _cost: number, context: MutationContext | undefined) => {
67
+ if (context?.capturedUserId && context.previousCredits && !context.skippedOptimistic) {
68
+ queryClient.setQueryData(
69
+ creditsQueryKeys.user(context.capturedUserId),
70
+ context.previousCredits
71
+ );
72
+ }
73
+ },
74
+ onSuccess: (_data: unknown, _cost: number, context: MutationContext | undefined) => {
75
+ const targetUserId = context?.capturedUserId ?? userId;
76
+ if (targetUserId) {
77
+ queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(targetUserId) });
78
+ }
79
+ },
80
+ };
81
+ }
@@ -0,0 +1,11 @@
1
+ export interface UseDeductCreditParams {
2
+ userId: string | undefined;
3
+ onCreditsExhausted?: () => void;
4
+ }
5
+
6
+ export interface UseDeductCreditResult {
7
+ checkCredits: (cost?: number) => Promise<boolean>;
8
+ deductCredit: (cost?: number) => Promise<boolean>;
9
+ deductCredits: (cost: number) => Promise<boolean>;
10
+ isDeducting: boolean;
11
+ }
@@ -0,0 +1,44 @@
1
+ import { useCallback } from "react";
2
+ import { useMutation, useQueryClient } from "@umituz/react-native-design-system";
3
+ import { getCreditsRepository } from "../../infrastructure/CreditsRepositoryManager";
4
+ import type { UseDeductCreditParams, UseDeductCreditResult } from "./types";
5
+ import { createDeductCreditMutationConfig } from "./mutationConfig";
6
+
7
+ export const useDeductCredit = ({
8
+ userId,
9
+ onCreditsExhausted,
10
+ }: UseDeductCreditParams): UseDeductCreditResult => {
11
+ const repository = getCreditsRepository();
12
+ const queryClient = useQueryClient();
13
+
14
+ const mutation = useMutation(createDeductCreditMutationConfig(userId, repository, queryClient));
15
+
16
+ const deductCredit = useCallback(async (cost: number = 1): Promise<boolean> => {
17
+ try {
18
+ const res = await mutation.mutateAsync(cost);
19
+ if (!res.success) {
20
+ if (res.error?.code === "CREDITS_EXHAUSTED") onCreditsExhausted?.();
21
+ return false;
22
+ }
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }, [mutation, onCreditsExhausted]);
28
+
29
+ const deductCredits = useCallback(async (cost: number): Promise<boolean> => {
30
+ return await deductCredit(cost);
31
+ }, [deductCredit]);
32
+
33
+ const checkCredits = useCallback(async (cost: number = 1): Promise<boolean> => {
34
+ if (!userId) return false;
35
+ return repository.hasCredits(userId, cost);
36
+ }, [userId, repository]);
37
+
38
+ return {
39
+ checkCredits,
40
+ deductCredit,
41
+ deductCredits,
42
+ isDeducting: mutation.isPending
43
+ };
44
+ };
@@ -0,0 +1,21 @@
1
+ import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
2
+ import { getCurrentUserId, setupAuthStateListener } from "../SubscriptionAuthListener";
3
+ import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
4
+
5
+ export async function startBackgroundInitialization(config: SubscriptionInitConfig): Promise<void> {
6
+ const initializeInBackground = async (userId?: string): Promise<void> => {
7
+ await SubscriptionManager.initialize(userId);
8
+ };
9
+
10
+ const auth = config.getFirebaseAuth();
11
+ if (!auth) {
12
+ throw new Error("Firebase auth is not available");
13
+ }
14
+
15
+ const initialUserId = getCurrentUserId(() => auth);
16
+ await initializeInBackground(initialUserId);
17
+
18
+ setupAuthStateListener(() => auth, (newUserId) => {
19
+ initializeInBackground(newUserId);
20
+ });
21
+ }
@@ -0,0 +1,33 @@
1
+ import { Platform } from "react-native";
2
+ import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
3
+
4
+ export function getApiKey(config: SubscriptionInitConfig): string {
5
+ const { apiKey, apiKeyIos, apiKeyAndroid } = config;
6
+ const key = Platform.OS === 'ios'
7
+ ? (apiKeyIos || apiKey)
8
+ : (apiKeyAndroid || apiKey);
9
+
10
+ if (!key) {
11
+ throw new Error('API key required');
12
+ }
13
+
14
+ return key;
15
+ }
16
+
17
+ export function validateConfig(config: SubscriptionInitConfig): void {
18
+ if (!config.creditPackages) {
19
+ throw new Error('creditPackages is required');
20
+ }
21
+
22
+ if (!config.creditPackages.identifierPattern) {
23
+ throw new Error('creditPackages.identifierPattern is required');
24
+ }
25
+
26
+ if (!config.creditPackages.amounts) {
27
+ throw new Error('creditPackages.amounts is required');
28
+ }
29
+
30
+ if (!config.getAnonymousUserId) {
31
+ throw new Error('getAnonymousUserId is required');
32
+ }
33
+ }
@@ -0,0 +1,45 @@
1
+ import { configureCreditsRepository } from "../../../credits/infrastructure/CreditsRepositoryManager";
2
+ import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
3
+ import { configureAuthProvider } from "../../presentation/useAuthAwarePurchase";
4
+ import { SubscriptionSyncService } from "../SubscriptionSyncService";
5
+ import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
6
+
7
+ export function configureServices(config: SubscriptionInitConfig, apiKey: string): SubscriptionSyncService {
8
+ const { entitlementId, credits, creditPackages, getAnonymousUserId, getFirebaseAuth, showAuthModal, onCreditsUpdated } = config;
9
+
10
+ configureCreditsRepository({
11
+ ...credits,
12
+ creditPackageAmounts: creditPackages!.amounts
13
+ });
14
+
15
+ const syncService = new SubscriptionSyncService(entitlementId);
16
+
17
+ SubscriptionManager.configure({
18
+ config: {
19
+ apiKey,
20
+ entitlementIdentifier: entitlementId,
21
+ consumableProductIdentifiers: [creditPackages!.identifierPattern],
22
+ onPurchaseCompleted: (u: string, p: string, c: any, s: any) => syncService.handlePurchase(u, p, c, s),
23
+ onRenewalDetected: (u: string, p: string, expires: string, c: any) => syncService.handleRenewal(u, p, expires, c),
24
+ onPremiumStatusChanged: (u: string, isP: boolean, pId: any, exp: any, willR: any, pt: any) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt),
25
+ onCreditsUpdated,
26
+ },
27
+ apiKey,
28
+ getAnonymousUserId: getAnonymousUserId!,
29
+ });
30
+
31
+ configureAuthProvider({
32
+ isAuthenticated: () => {
33
+ const auth = getFirebaseAuth();
34
+ if (!auth) {
35
+ throw new Error("Firebase auth is not available");
36
+ }
37
+
38
+ const u = auth.currentUser;
39
+ return !!(u && !u.isAnonymous);
40
+ },
41
+ showAuthModal,
42
+ });
43
+
44
+ return syncService;
45
+ }
@@ -0,0 +1,11 @@
1
+ import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
2
+ import { getApiKey, validateConfig } from "./ConfigValidator";
3
+ import { configureServices } from "./ServiceConfigurator";
4
+ import { startBackgroundInitialization } from "./BackgroundInitializer";
5
+
6
+ export const initializeSubscription = async (config: SubscriptionInitConfig): Promise<void> => {
7
+ const apiKey = getApiKey(config);
8
+ validateConfig(config);
9
+ configureServices(config, apiKey);
10
+ await startBackgroundInitialization(config);
11
+ };
@@ -0,0 +1,2 @@
1
+ export { initializeSubscription } from "./SubscriptionInitializer";
2
+ export type { FirebaseAuthLike, CreditPackageConfig, SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
@@ -1,19 +1,13 @@
1
- /**
2
- * Package Handler
3
- * Handles operations: fetch, purchase, restore
4
- */
5
-
6
1
  import type { PurchasesPackage, CustomerInfo } from "react-native-purchases";
7
2
  import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
8
- import { getPremiumEntitlement } from "../../../revenuecat/core/types";
9
- import { PurchaseStatusResolver, type PremiumStatus } from "./PurchaseStatusResolver";
10
-
11
- declare const __DEV__: boolean;
12
-
13
- export interface RestoreResultInfo {
14
- success: boolean;
15
- productId: string | null;
16
- }
3
+ import type { PremiumStatus } from "./PurchaseStatusResolver";
4
+ import {
5
+ fetchPackages,
6
+ executePurchase,
7
+ restorePurchases,
8
+ checkPremiumStatus,
9
+ type RestoreResultInfo,
10
+ } from "./package-operations";
17
11
 
18
12
  export class PackageHandler {
19
13
  constructor(
@@ -26,95 +20,20 @@ export class PackageHandler {
26
20
  }
27
21
 
28
22
  async fetchPackages(): Promise<PurchasesPackage[]> {
29
- if (!this.service.isInitialized()) {
30
- throw new Error("Service not initialized. Please initialize before fetching packages.");
31
- }
32
-
33
- try {
34
- const offering = await this.service.fetchOfferings();
35
-
36
- if (__DEV__) {
37
- console.log('[PackageHandler] fetchOfferings result:', {
38
- hasOffering: !!offering,
39
- offeringId: offering?.identifier,
40
- packagesCount: offering?.availablePackages?.length,
41
- });
42
- }
43
-
44
- if (!offering) {
45
- if (__DEV__) {
46
- console.warn('[PackageHandler] No offering returned, returning empty array');
47
- }
48
- return [];
49
- }
50
-
51
- const packages = offering.availablePackages;
52
- if (!packages || packages.length === 0) {
53
- if (__DEV__) {
54
- console.warn('[PackageHandler] Offering has no packages, returning empty array');
55
- }
56
- return [];
57
- }
58
-
59
- if (__DEV__) {
60
- console.log('[PackageHandler] Returning packages:', {
61
- count: packages.length,
62
- packageIds: packages.map(p => p.product.identifier),
63
- });
64
- }
65
-
66
- return packages;
67
- } catch (error) {
68
- if (__DEV__) {
69
- console.error('[PackageHandler] Error fetching packages:', error);
70
- }
71
- throw new Error(
72
- `Failed to fetch subscription packages. ${
73
- error instanceof Error ? error.message : "Unknown error"
74
- }`
75
- );
76
- }
23
+ return fetchPackages(this.service);
77
24
  }
78
25
 
79
26
  async purchase(pkg: PurchasesPackage, userId: string): Promise<boolean> {
80
- if (!this.service.isInitialized()) {
81
- throw new Error("Service not initialized");
82
- }
83
-
84
- const result = await this.service.purchasePackage(pkg, userId);
85
- return result.success;
27
+ return executePurchase(this.service, pkg, userId);
86
28
  }
87
29
 
88
30
  async restore(userId: string): Promise<RestoreResultInfo> {
89
- if (!this.service.isInitialized()) {
90
- throw new Error("Service not initialized");
91
- }
92
-
93
- const result = await this.service.restorePurchases(userId);
94
-
95
- if (!result.success) {
96
- return { success: false, productId: null };
97
- }
98
-
99
- if (!result.customerInfo) {
100
- return { success: true, productId: null };
101
- }
102
-
103
- const entitlement = getPremiumEntitlement(result.customerInfo, this.entitlementId);
104
-
105
- if (!entitlement) {
106
- return { success: true, productId: null };
107
- }
108
-
109
- return {
110
- success: true,
111
- productId: entitlement.productIdentifier,
112
- };
31
+ return restorePurchases(this.service, userId, this.entitlementId);
113
32
  }
114
33
 
115
34
  checkPremiumStatusFromInfo(customerInfo: CustomerInfo): PremiumStatus {
116
- return PurchaseStatusResolver.resolve(customerInfo, this.entitlementId);
35
+ return checkPremiumStatus(customerInfo, this.entitlementId);
117
36
  }
118
37
  }
119
38
 
120
- export type { PremiumStatus };
39
+ export type { PremiumStatus, RestoreResultInfo };
@@ -0,0 +1,57 @@
1
+ import type { PurchasesPackage } from "react-native-purchases";
2
+ import type { IRevenueCatService } from "../../../../../shared/application/ports/IRevenueCatService";
3
+
4
+ declare const __DEV__: boolean;
5
+
6
+ export async function fetchPackages(
7
+ service: IRevenueCatService
8
+ ): Promise<PurchasesPackage[]> {
9
+ if (!service.isInitialized()) {
10
+ throw new Error("Service not initialized. Please initialize before fetching packages.");
11
+ }
12
+
13
+ try {
14
+ const offering = await service.fetchOfferings();
15
+
16
+ if (__DEV__) {
17
+ console.log('[PackageHandler] fetchOfferings result:', {
18
+ hasOffering: !!offering,
19
+ offeringId: offering?.identifier,
20
+ packagesCount: offering?.availablePackages?.length,
21
+ });
22
+ }
23
+
24
+ if (!offering) {
25
+ if (__DEV__) {
26
+ console.warn('[PackageHandler] No offering returned, returning empty array');
27
+ }
28
+ return [];
29
+ }
30
+
31
+ const packages = offering.availablePackages;
32
+ if (!packages || packages.length === 0) {
33
+ if (__DEV__) {
34
+ console.warn('[PackageHandler] Offering has no packages, returning empty array');
35
+ }
36
+ return [];
37
+ }
38
+
39
+ if (__DEV__) {
40
+ console.log('[PackageHandler] Returning packages:', {
41
+ count: packages.length,
42
+ packageIds: packages.map(p => p.product.identifier),
43
+ });
44
+ }
45
+
46
+ return packages;
47
+ } catch (error) {
48
+ if (__DEV__) {
49
+ console.error('[PackageHandler] Error fetching packages:', error);
50
+ }
51
+ throw new Error(
52
+ `Failed to fetch subscription packages. ${
53
+ error instanceof Error ? error.message : "Unknown error"
54
+ }`
55
+ );
56
+ }
57
+ }
@@ -0,0 +1,15 @@
1
+ import type { PurchasesPackage } from "react-native-purchases";
2
+ import type { IRevenueCatService } from "../../../../../shared/application/ports/IRevenueCatService";
3
+
4
+ export async function executePurchase(
5
+ service: IRevenueCatService,
6
+ pkg: PurchasesPackage,
7
+ userId: string
8
+ ): Promise<boolean> {
9
+ if (!service.isInitialized()) {
10
+ throw new Error("Service not initialized");
11
+ }
12
+
13
+ const result = await service.purchasePackage(pkg, userId);
14
+ return result.success;
15
+ }
@@ -0,0 +1,34 @@
1
+ import type { IRevenueCatService } from "../../../../../shared/application/ports/IRevenueCatService";
2
+ import { getPremiumEntitlement } from "../../../../revenuecat/core/types";
3
+ import type { RestoreResultInfo } from "./types";
4
+
5
+ export async function restorePurchases(
6
+ service: IRevenueCatService,
7
+ userId: string,
8
+ entitlementId: string
9
+ ): Promise<RestoreResultInfo> {
10
+ if (!service.isInitialized()) {
11
+ throw new Error("Service not initialized");
12
+ }
13
+
14
+ const result = await service.restorePurchases(userId);
15
+
16
+ if (!result.success) {
17
+ return { success: false, productId: null };
18
+ }
19
+
20
+ if (!result.customerInfo) {
21
+ return { success: true, productId: null };
22
+ }
23
+
24
+ const entitlement = getPremiumEntitlement(result.customerInfo, entitlementId);
25
+
26
+ if (!entitlement) {
27
+ return { success: true, productId: null };
28
+ }
29
+
30
+ return {
31
+ success: true,
32
+ productId: entitlement.productIdentifier,
33
+ };
34
+ }
@@ -0,0 +1,9 @@
1
+ import type { CustomerInfo } from "react-native-purchases";
2
+ import { PurchaseStatusResolver, type PremiumStatus } from "../PurchaseStatusResolver";
3
+
4
+ export function checkPremiumStatus(
5
+ customerInfo: CustomerInfo,
6
+ entitlementId: string
7
+ ): PremiumStatus {
8
+ return PurchaseStatusResolver.resolve(customerInfo, entitlementId);
9
+ }
@@ -0,0 +1,5 @@
1
+ export type { RestoreResultInfo } from "./types";
2
+ export { fetchPackages } from "./PackageFetcher";
3
+ export { executePurchase } from "./PackagePurchaser";
4
+ export { restorePurchases } from "./PackageRestorer";
5
+ export { checkPremiumStatus } from "./PremiumStatusChecker";
@@ -0,0 +1,4 @@
1
+ export interface RestoreResultInfo {
2
+ success: boolean;
3
+ productId: string | null;
4
+ }
@@ -0,0 +1,2 @@
1
+ export { useCustomerInfo } from "./useCustomerInfo";
2
+ export type { UseCustomerInfoResult } from "./types";
@@ -0,0 +1,9 @@
1
+ import type { CustomerInfo } from "react-native-purchases";
2
+
3
+ export interface UseCustomerInfoResult {
4
+ customerInfo: CustomerInfo | null;
5
+ loading: boolean;
6
+ error: string | null;
7
+ refetch: () => Promise<void>;
8
+ isFetching: boolean;
9
+ }
@@ -0,0 +1,57 @@
1
+ import { useEffect, useState, useCallback, useRef } from "react";
2
+ import Purchases, { type CustomerInfo } from "react-native-purchases";
3
+ import type { UseCustomerInfoResult } from "./types";
4
+
5
+ export function useCustomerInfo(): UseCustomerInfoResult {
6
+ const [customerInfo, setCustomerInfo] = useState<CustomerInfo | null>(null);
7
+ const [loading, setLoading] = useState(true);
8
+ const [isFetching, setIsFetching] = useState(false);
9
+ const [error, setError] = useState<string | null>(null);
10
+
11
+ const fetchCustomerInfo = useCallback(async () => {
12
+ try {
13
+ setIsFetching(true);
14
+ setError(null);
15
+
16
+ const info = await Purchases.getCustomerInfo();
17
+
18
+ setCustomerInfo(info);
19
+ } catch (err) {
20
+ const errorMessage =
21
+ err instanceof Error ? err.message : "Failed to fetch customer info";
22
+ setError(errorMessage);
23
+ } finally {
24
+ setLoading(false);
25
+ setIsFetching(false);
26
+ }
27
+ }, []);
28
+
29
+ const listenerRef = useRef<((info: CustomerInfo) => void) | null>(null);
30
+
31
+ useEffect(() => {
32
+ fetchCustomerInfo();
33
+
34
+ const listener = (info: CustomerInfo) => {
35
+ setCustomerInfo(info);
36
+ setError(null);
37
+ };
38
+
39
+ listenerRef.current = listener;
40
+ Purchases.addCustomerInfoUpdateListener(listener);
41
+
42
+ return () => {
43
+ if (listenerRef.current) {
44
+ Purchases.removeCustomerInfoUpdateListener(listenerRef.current);
45
+ listenerRef.current = null;
46
+ }
47
+ };
48
+ }, [fetchCustomerInfo]);
49
+
50
+ return {
51
+ customerInfo,
52
+ loading,
53
+ error,
54
+ refetch: fetchCustomerInfo,
55
+ isFetching,
56
+ };
57
+ }
@@ -1,7 +1,7 @@
1
1
  import type { CustomerInfo } from "react-native-purchases";
2
2
  import type { RevenueCatConfig } from "../../../../revenuecat/core/types";
3
3
  import { syncPremiumStatus } from "../../utils/PremiumStatusSyncer";
4
- import { detectRenewal, updateRenewalState, type RenewalState } from "../../utils/RenewalDetector";
4
+ import { detectRenewal, updateRenewalState, type RenewalState } from "../../utils/renewal";
5
5
 
6
6
  async function handleRenewal(
7
7
  userId: string,
@@ -1,5 +1,5 @@
1
1
  import type { CustomerInfoUpdateListener } from "react-native-purchases";
2
- import type { RenewalState } from "../../utils/RenewalDetector";
2
+ import type { RenewalState } from "../../utils/renewal";
3
3
 
4
4
  export class ListenerState {
5
5
  listener: CustomerInfoUpdateListener | null = null;
@@ -7,7 +7,6 @@ import {
7
7
  import {
8
8
  isUserCancelledError,
9
9
  isNetworkError,
10
- isAlreadyPurchasedError,
11
10
  isInvalidCredentialsError,
12
11
  getRawErrorMessage,
13
12
  getErrorCode,
@@ -0,0 +1,14 @@
1
+ import { detectPackageType } from "../../../../../utils/packageTypeDetector";
2
+
3
+ const PACKAGE_TIER_ORDER: Record<string, number> = {
4
+ weekly: 1,
5
+ monthly: 2,
6
+ yearly: 3,
7
+ unknown: 0,
8
+ };
9
+
10
+ export function getPackageTier(productId: string | null): number {
11
+ if (!productId) return 0;
12
+ const packageType = detectPackageType(productId);
13
+ return PACKAGE_TIER_ORDER[packageType] ?? 0;
14
+ }