@umituz/react-native-subscription 2.27.56 → 2.27.58

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 (47) hide show
  1. package/package.json +1 -1
  2. package/src/domain/errors/SubscriptionError.ts +3 -0
  3. package/src/domain/types/RevenueCatData.ts +3 -1
  4. package/src/domains/config/domain/entities/Plan.ts +2 -2
  5. package/src/domains/paywall/components/PaywallModal.tsx +6 -3
  6. package/src/domains/paywall/components/PlanCard.tsx +1 -1
  7. package/src/domains/paywall/hooks/usePaywallActions.ts +0 -2
  8. package/src/domains/wallet/domain/mappers/TransactionMapper.ts +2 -2
  9. package/src/domains/wallet/infrastructure/services/ProductMetadataService.ts +0 -2
  10. package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +0 -2
  11. package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +0 -2
  12. package/src/infrastructure/models/UserCreditsDocument.ts +7 -17
  13. package/src/infrastructure/repositories/CreditsRepository.ts +5 -3
  14. package/src/infrastructure/services/ActivationHandler.ts +15 -4
  15. package/src/infrastructure/services/CreditsInitializer.ts +5 -3
  16. package/src/infrastructure/services/SubscriptionInitializer.ts +0 -1
  17. package/src/infrastructure/services/SubscriptionService.ts +2 -8
  18. package/src/infrastructure/services/TrialService.ts +0 -1
  19. package/src/infrastructure/services/app-service-helpers.ts +0 -2
  20. package/src/infrastructure/utils/Logger.ts +0 -2
  21. package/src/init/createSubscriptionInitModule.ts +1 -3
  22. package/src/presentation/hooks/feedback/useFeedbackSubmit.ts +33 -24
  23. package/src/presentation/hooks/useAuthAwarePurchase.ts +0 -2
  24. package/src/presentation/hooks/useAuthSubscriptionSync.ts +9 -5
  25. package/src/presentation/hooks/useCredits.ts +0 -2
  26. package/src/presentation/hooks/useDeductCredit.ts +0 -2
  27. package/src/presentation/hooks/useFeatureGate.ts +0 -2
  28. package/src/presentation/hooks/usePremium.ts +0 -2
  29. package/src/presentation/hooks/useSavedPurchaseAutoExecution.ts +5 -5
  30. package/src/presentation/screens/SubscriptionDetailScreen.tsx +6 -6
  31. package/src/presentation/stores/purchaseLoadingStore.ts +0 -2
  32. package/src/revenuecat/application/ports/IRevenueCatService.ts +1 -1
  33. package/src/revenuecat/infrastructure/handlers/PackageHandler.ts +0 -2
  34. package/src/revenuecat/infrastructure/managers/SubscriptionManager.ts +0 -2
  35. package/src/revenuecat/infrastructure/services/CustomerInfoListenerManager.ts +0 -2
  36. package/src/revenuecat/infrastructure/services/PurchaseHandler.ts +0 -2
  37. package/src/revenuecat/infrastructure/services/RestoreHandler.ts +1 -1
  38. package/src/revenuecat/infrastructure/services/RevenueCatInitializer.ts +7 -9
  39. package/src/revenuecat/infrastructure/services/RevenueCatService.ts +3 -1
  40. package/src/revenuecat/infrastructure/utils/PremiumStatusSyncer.ts +0 -2
  41. package/src/revenuecat/infrastructure/utils/RenewalDetector.ts +9 -1
  42. package/src/revenuecat/presentation/hooks/usePurchasePackage.ts +0 -2
  43. package/src/revenuecat/presentation/hooks/useRevenueCatTrialEligibility.ts +0 -2
  44. package/src/utils/premiumStatusUtils.ts +5 -4
  45. package/src/utils/priceUtils.ts +10 -15
  46. package/src/utils/tierUtils.ts +3 -2
  47. package/src/presentation/hooks/useSubscription.utils.ts +0 -79
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.27.56",
3
+ "version": "2.27.58",
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",
@@ -10,6 +10,7 @@ export class SubscriptionError extends Error {
10
10
  super(message);
11
11
  this.name = 'SubscriptionError';
12
12
  this.code = code;
13
+ Object.setPrototypeOf(this, SubscriptionError.prototype);
13
14
  }
14
15
 
15
16
  static notFound(message: string = 'Subscription not found'): SubscriptionError {
@@ -40,6 +41,7 @@ export class SubscriptionRepositoryError extends Error {
40
41
  super(message);
41
42
  this.name = 'SubscriptionRepositoryError';
42
43
  this.code = code;
44
+ Object.setPrototypeOf(this, SubscriptionRepositoryError.prototype);
43
45
  }
44
46
  }
45
47
 
@@ -50,5 +52,6 @@ export class SubscriptionValidationError extends Error {
50
52
  super(message);
51
53
  this.name = 'SubscriptionValidationError';
52
54
  this.code = code;
55
+ Object.setPrototypeOf(this, SubscriptionValidationError.prototype);
53
56
  }
54
57
  }
@@ -1,3 +1,5 @@
1
+ import type { PeriodType } from "../entities/SubscriptionStatus";
2
+
1
3
  /**
2
4
  * RevenueCat subscription data (Single Source of Truth)
3
5
  * Used across the subscription package for storing RevenueCat data in Firestore
@@ -8,5 +10,5 @@ export interface RevenueCatData {
8
10
  originalTransactionId?: string;
9
11
  isPremium?: boolean;
10
12
  /** RevenueCat period type: NORMAL, INTRO, or TRIAL */
11
- periodType?: "NORMAL" | "INTRO" | "TRIAL";
13
+ periodType?: PeriodType;
12
14
  }
@@ -32,8 +32,8 @@ export const calculatePlanMetadata = (
32
32
  const totalCost = plan.credits * costPerCredit;
33
33
  const netRevenue = plan.price * (1 - commissionRate);
34
34
  const profit = netRevenue - totalCost;
35
- const profitMargin = (profit / plan.price) * 100;
36
- const pricePerCredit = plan.price / plan.credits;
35
+ const profitMargin = plan.price > 0 ? (profit / plan.price) * 100 : 0;
36
+ const pricePerCredit = plan.credits > 0 ? plan.price / plan.credits : 0;
37
37
 
38
38
  return {
39
39
  cost: totalCost,
@@ -2,7 +2,7 @@
2
2
  * Paywall Modal
3
3
  */
4
4
 
5
- import React, { useState, useCallback } from "react";
5
+ import React, { useState, useCallback, useEffect } from "react";
6
6
  import { View, ScrollView, TouchableOpacity, Linking, type ImageSourcePropType } from "react-native";
7
7
  import { BaseModal, useAppDesignTokens, AtomicText, AtomicIcon, AtomicSpinner } from "@umituz/react-native-design-system";
8
8
  import { Image } from "expo-image";
@@ -14,8 +14,6 @@ import { PaywallFeatures } from "./PaywallFeatures";
14
14
  import { PaywallFooter } from "./PaywallFooter";
15
15
  import { usePurchaseLoadingStore, selectIsPurchasing } from "../../../presentation/stores";
16
16
 
17
- declare const __DEV__: boolean;
18
-
19
17
  /** Trial eligibility info per product */
20
18
  export interface TrialEligibilityInfo {
21
19
  /** Whether eligible for trial */
@@ -56,6 +54,11 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
56
54
  const isGlobalPurchasing = usePurchaseLoadingStore(selectIsPurchasing);
57
55
  const { startPurchase, endPurchase } = usePurchaseLoadingStore();
58
56
 
57
+ // Reset selected plan when packages change
58
+ useEffect(() => {
59
+ setSelectedPlanId(null);
60
+ }, [packages]);
61
+
59
62
  // Combined processing state
60
63
  const isProcessing = isLocalProcessing || isGlobalPurchasing;
61
64
 
@@ -78,7 +78,7 @@ export const PlanCard: React.FC<PlanCardProps> = React.memo(
78
78
  </AtomicText>
79
79
 
80
80
  {/* Credits info */}
81
- {creditAmount && creditsLabel && (
81
+ {creditAmount != null && creditAmount > 0 && creditsLabel && (
82
82
  <AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
83
83
  {creditAmount} {creditsLabel}
84
84
  </AtomicText>
@@ -4,8 +4,6 @@ import { useRestorePurchase } from "../../../revenuecat/presentation/hooks/useRe
4
4
  import { useAuthAwarePurchase } from "../../../presentation/hooks/useAuthAwarePurchase";
5
5
  import type { PurchaseSource } from "../../../domain/entities/Credits";
6
6
 
7
- declare const __DEV__: boolean;
8
-
9
7
  interface UsePaywallActionsProps {
10
8
  source?: PurchaseSource;
11
9
  onPurchaseSuccess?: () => void;
@@ -11,7 +11,7 @@ export class TransactionMapper {
11
11
  const data = docSnap.data();
12
12
  return {
13
13
  id: docSnap.id,
14
- userId: data.userId || defaultUserId,
14
+ userId: data.userId ?? defaultUserId,
15
15
  change: data.change,
16
16
  reason: data.reason,
17
17
  feature: data.feature,
@@ -19,7 +19,7 @@ export class TransactionMapper {
19
19
  packageId: data.packageId,
20
20
  subscriptionPlan: data.subscriptionPlan,
21
21
  description: data.description,
22
- createdAt: data.createdAt?.toMillis?.() || Date.now(),
22
+ createdAt: data.createdAt?.toMillis?.() ?? Date.now(),
23
23
  };
24
24
  }
25
25
 
@@ -13,8 +13,6 @@ import type {
13
13
  ProductType,
14
14
  } from "../../domain/types/wallet.types";
15
15
 
16
- declare const __DEV__: boolean;
17
-
18
16
  interface CacheEntry {
19
17
  data: ProductMetadata[];
20
18
  timestamp: number;
@@ -14,8 +14,6 @@ import type {
14
14
  } from "../../domain/types/wallet.types";
15
15
  import { ProductMetadataService } from "../../infrastructure/services/ProductMetadataService";
16
16
 
17
- declare const __DEV__: boolean;
18
-
19
17
  const CACHE_CONFIG = {
20
18
  staleTime: 5 * 60 * 1000, // 5 minutes
21
19
  gcTime: 30 * 60 * 1000, // 30 minutes
@@ -17,8 +17,6 @@ import type {
17
17
  } from "../../domain/types/transaction.types";
18
18
  import { TransactionRepository } from "../../infrastructure/repositories/TransactionRepository";
19
19
 
20
- declare const __DEV__: boolean;
21
-
22
20
  export const transactionQueryKeys = {
23
21
  all: ["transactions"] as const,
24
22
  user: (userId: string) => ["transactions", userId] as const,
@@ -1,23 +1,13 @@
1
+ import type { PurchaseSource, PurchaseType } from "../../domain/entities/Credits";
2
+ import type { SubscriptionStatusType, PeriodType } from "../../domain/entities/SubscriptionStatus";
3
+
4
+ export type { PurchaseSource, PurchaseType } from "../../domain/entities/Credits";
5
+ export type { SubscriptionStatusType, PeriodType } from "../../domain/entities/SubscriptionStatus";
6
+
1
7
  export interface FirestoreTimestamp {
2
8
  toDate: () => Date;
3
9
  }
4
10
 
5
- export type PurchaseSource =
6
- | "onboarding"
7
- | "settings"
8
- | "upgrade_prompt"
9
- | "home_screen"
10
- | "feature_gate"
11
- | "credits_exhausted"
12
- | "renewal";
13
-
14
- export type PurchaseType = "initial" | "renewal" | "upgrade" | "downgrade";
15
-
16
- export type SubscriptionDocStatus = "active" | "trial" | "trial_canceled" | "expired" | "canceled" | "free";
17
-
18
- /** RevenueCat period types */
19
- export type PeriodType = "NORMAL" | "INTRO" | "TRIAL";
20
-
21
11
  export interface PurchaseMetadata {
22
12
  productId: string;
23
13
  packageType: "weekly" | "monthly" | "yearly" | "lifetime";
@@ -33,7 +23,7 @@ export interface PurchaseMetadata {
33
23
  export interface UserCreditsDocumentRead {
34
24
  // Core subscription status
35
25
  isPremium?: boolean;
36
- status?: SubscriptionDocStatus;
26
+ status?: SubscriptionStatusType;
37
27
 
38
28
  // Dates (all from RevenueCat)
39
29
  purchasedAt?: FirestoreTimestamp;
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Credits Repository
3
3
  */
4
- declare const __DEV__: boolean;
5
4
 
6
5
  import { doc, getDoc, runTransaction, serverTimestamp, type Firestore, type Transaction } from "firebase/firestore";
7
6
  import { BaseRepository, getFirestore } from "@umituz/react-native-firebase";
@@ -76,10 +75,13 @@ export class CreditsRepository extends BaseRepository {
76
75
  periodType: revenueCatData?.periodType,
77
76
  };
78
77
 
79
- const res = await initializeCreditsTransaction(db, this.getRef(db, userId), cfg, purchaseId, metadata);
78
+ await initializeCreditsTransaction(db, this.getRef(db, userId), cfg, purchaseId, metadata);
79
+ // Re-fetch from Firestore to get the actual stored data with all fields
80
+ const snap = await getDoc(this.getRef(db, userId));
81
+ const fullData = snap.exists() ? snap.data() as UserCreditsDocumentRead : undefined;
80
82
  return {
81
83
  success: true,
82
- data: CreditsMapper.toEntity({ ...res, purchasedAt: undefined, lastUpdatedAt: undefined })
84
+ data: fullData ? CreditsMapper.toEntity(fullData) : undefined,
83
85
  };
84
86
  } catch (e: unknown) {
85
87
  const message = e instanceof Error ? e.message : String(e);
@@ -81,17 +81,28 @@ async function notifyStatusChange(
81
81
  }
82
82
  }
83
83
 
84
- async function handleError(
85
- config: ActivationHandlerConfig,
84
+ /**
85
+ * Safe error handler - wraps error callbacks to prevent secondary failures
86
+ */
87
+ export async function safeHandleError(
88
+ onError: ((error: Error, context: string) => Promise<void> | void) | undefined,
86
89
  error: unknown,
87
90
  context: string
88
91
  ): Promise<void> {
89
- if (!config.onError) return;
92
+ if (!onError) return;
90
93
 
91
94
  try {
92
95
  const err = error instanceof Error ? error : new Error("Unknown error");
93
- await config.onError(err, `ActivationHandler.${context}`);
96
+ await onError(err, context);
94
97
  } catch {
95
98
  // Ignore callback errors
96
99
  }
97
100
  }
101
+
102
+ async function handleError(
103
+ config: ActivationHandlerConfig,
104
+ error: unknown,
105
+ context: string
106
+ ): Promise<void> {
107
+ await safeHandleError(config.onError, error, `ActivationHandler.${context}`);
108
+ }
@@ -74,7 +74,7 @@ export async function initializeCreditsTransaction(
74
74
  const allocation = packageType && packageType !== "unknown"
75
75
  ? getCreditAllocation(packageType, config.packageAllocations)
76
76
  : null;
77
- const creditLimit = allocation || config.creditLimit;
77
+ const creditLimit = allocation ?? config.creditLimit;
78
78
 
79
79
  const platform = Platform.OS as "ios" | "android";
80
80
  const appVersion = Constants.expoConfig?.version;
@@ -96,7 +96,7 @@ export async function initializeCreditsTransaction(
96
96
  type: purchaseType,
97
97
  platform,
98
98
  appVersion,
99
- timestamp: Date.now() as unknown as PurchaseMetadata["timestamp"],
99
+ timestamp: Timestamp.fromDate(new Date()) as unknown as PurchaseMetadata["timestamp"],
100
100
  } : undefined;
101
101
 
102
102
  const purchaseHistory = purchaseMetadata
@@ -107,7 +107,9 @@ export async function initializeCreditsTransaction(
107
107
  const willRenew = metadata?.willRenew;
108
108
  const periodType = metadata?.periodType;
109
109
 
110
- const status = resolveSubscriptionStatus({ isPremium, willRenew, isExpired: !isPremium, periodType });
110
+ const expirationDateStr = metadata?.expirationDate;
111
+ const isExpired = expirationDateStr ? new Date(expirationDateStr).getTime() < Date.now() : false;
112
+ const status = resolveSubscriptionStatus({ isPremium, willRenew, isExpired, periodType });
111
113
 
112
114
  // Determine if this is a status sync (not a new purchase or renewal)
113
115
  // Status sync should preserve existing credits, only update metadata
@@ -6,7 +6,6 @@
6
6
  * - Relies on CustomerInfoUpdateListener for state updates
7
7
  * - No manual timeouts - uses auth state listener with cleanup
8
8
  */
9
- declare const __DEV__: boolean;
10
9
 
11
10
  import { Platform } from "react-native";
12
11
  import type { CustomerInfo } from "react-native-purchases";
@@ -16,6 +16,7 @@ import type { SubscriptionConfig } from "../../domain/value-objects/Subscription
16
16
  import {
17
17
  activateSubscription,
18
18
  deactivateSubscription,
19
+ safeHandleError,
19
20
  type ActivationHandlerConfig,
20
21
  } from "./ActivationHandler";
21
22
 
@@ -108,14 +109,7 @@ export class SubscriptionService implements ISubscriptionService {
108
109
  }
109
110
 
110
111
  private async handleError(error: unknown, context: string): Promise<void> {
111
- if (!this.handlerConfig.onError) return;
112
-
113
- try {
114
- const err = error instanceof Error ? error : new Error("Unknown error");
115
- await this.handlerConfig.onError(err, `SubscriptionService.${context}`);
116
- } catch {
117
- // Ignore callback errors
118
- }
112
+ await safeHandleError(this.handlerConfig.onError, error, `SubscriptionService.${context}`);
119
113
  }
120
114
  }
121
115
 
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Trial Service - Device-based trial tracking to prevent abuse
3
3
  */
4
- declare const __DEV__: boolean;
5
4
 
6
5
  import { doc, getDoc, setDoc, serverTimestamp, arrayUnion } from "firebase/firestore";
7
6
  import { getFirestore } from "@umituz/react-native-firebase";
@@ -17,8 +17,6 @@ import {
17
17
  selectUserId,
18
18
  } from "@umituz/react-native-auth";
19
19
 
20
- declare const __DEV__: boolean;
21
-
22
20
  export interface CreditServiceConfig {
23
21
  entitlementId: string;
24
22
  }
@@ -4,8 +4,6 @@
4
4
  * All logs are dev-only and automatically stripped in production.
5
5
  */
6
6
 
7
- declare const __DEV__: boolean;
8
-
9
7
  export type LogLevel = "debug" | "info" | "warn" | "error";
10
8
 
11
9
  export interface LogContext {
@@ -1,8 +1,6 @@
1
1
  import type { InitModule } from '@umituz/react-native-design-system';
2
2
  import { initializeSubscription, type SubscriptionInitConfig } from '../infrastructure/services/SubscriptionInitializer';
3
3
 
4
- declare const __DEV__: boolean;
5
-
6
4
  export interface SubscriptionInitModuleConfig extends Omit<SubscriptionInitConfig, 'apiKey'> {
7
5
  getApiKey: () => string | undefined;
8
6
  critical?: boolean;
@@ -29,7 +27,7 @@ export function createSubscriptionInitModule(config: SubscriptionInitModuleConfi
29
27
  return true;
30
28
  } catch (error) {
31
29
  if (__DEV__) console.error('[SubscriptionInit] Error:', error);
32
- return true;
30
+ return false;
33
31
  }
34
32
  },
35
33
  };
@@ -34,19 +34,23 @@ export function usePaywallFeedbackSubmit(
34
34
  });
35
35
  }
36
36
 
37
- const result = await submitPaywallFeedback(
38
- user?.uid ?? null,
39
- user?.email ?? null,
40
- reason
41
- );
42
-
43
- if (result.success) {
44
- onSuccess?.();
45
- } else if (result.error) {
46
- onError?.(result.error);
37
+ try {
38
+ const result = await submitPaywallFeedback(
39
+ user?.uid ?? null,
40
+ user?.email ?? null,
41
+ reason
42
+ );
43
+
44
+ if (result.success) {
45
+ onSuccess?.();
46
+ } else if (result.error) {
47
+ onError?.(result.error);
48
+ }
49
+ } catch (err) {
50
+ onError?.(err instanceof Error ? err : new Error("Feedback submission failed"));
51
+ } finally {
52
+ onComplete?.();
47
53
  }
48
-
49
- onComplete?.();
50
54
  },
51
55
  [user, onSuccess, onError, onComplete]
52
56
  );
@@ -84,19 +88,24 @@ export function useSettingsFeedbackSubmit(
84
88
  });
85
89
  }
86
90
 
87
- const result = await submitSettingsFeedback(
88
- user?.uid ?? null,
89
- user?.email ?? null,
90
- data
91
- );
92
-
93
- if (result.success) {
94
- onSuccess?.();
95
- } else if (result.error) {
96
- onError?.(result.error);
91
+ try {
92
+ const result = await submitSettingsFeedback(
93
+ user?.uid ?? null,
94
+ user?.email ?? null,
95
+ data
96
+ );
97
+
98
+ if (result.success) {
99
+ onSuccess?.();
100
+ } else if (result.error) {
101
+ onError?.(result.error);
102
+ }
103
+
104
+ return result;
105
+ } catch (err) {
106
+ onError?.(err instanceof Error ? err : new Error("Feedback submission failed"));
107
+ return { success: false, error: err instanceof Error ? err : new Error("Feedback submission failed") };
97
108
  }
98
-
99
- return result;
100
109
  },
101
110
  [user, onSuccess, onError]
102
111
  );
@@ -8,8 +8,6 @@ import type { PurchasesPackage } from "react-native-purchases";
8
8
  import { usePremium } from "./usePremium";
9
9
  import type { PurchaseSource } from "../../domain/entities/Credits";
10
10
 
11
- declare const __DEV__: boolean;
12
-
13
11
  export interface PurchaseAuthProvider {
14
12
  isAuthenticated: () => boolean;
15
13
  showAuthModal: () => void;
@@ -41,11 +41,15 @@ export function useAuthSubscriptionSync(
41
41
  return;
42
42
  }
43
43
 
44
- if (previousUserId && previousUserId !== userId) {
45
- await initialize(userId);
46
- } else if (!isInitializedRef.current) {
47
- await initialize(userId);
48
- isInitializedRef.current = true;
44
+ try {
45
+ if (previousUserId && previousUserId !== userId) {
46
+ await initialize(userId);
47
+ } else if (!isInitializedRef.current) {
48
+ await initialize(userId);
49
+ isInitializedRef.current = true;
50
+ }
51
+ } catch {
52
+ // Prevent unhandled promise rejection from async auth callback
49
53
  }
50
54
 
51
55
  previousUserIdRef.current = userId;
@@ -15,8 +15,6 @@ import {
15
15
  isCreditsRepositoryConfigured,
16
16
  } from "../../infrastructure/repositories/CreditsRepositoryProvider";
17
17
 
18
- declare const __DEV__: boolean;
19
-
20
18
  export const creditsQueryKeys = {
21
19
  all: ["credits"] as const,
22
20
  user: (userId: string) => ["credits", userId] as const,
@@ -11,8 +11,6 @@ import { creditsQueryKeys } from "./useCredits";
11
11
 
12
12
  import { timezoneService } from "@umituz/react-native-design-system";
13
13
 
14
- declare const __DEV__: boolean;
15
-
16
14
  export interface UseDeductCreditParams {
17
15
  userId: string | undefined;
18
16
  onCreditsExhausted?: () => void;
@@ -7,8 +7,6 @@
7
7
 
8
8
  import { useCallback, useRef, useEffect } from "react";
9
9
 
10
- declare const __DEV__: boolean;
11
-
12
10
  export interface UseFeatureGateParams {
13
11
  readonly isAuthenticated: boolean;
14
12
  readonly onShowAuthModal: (pendingCallback: () => void | Promise<void>) => void;
@@ -17,8 +17,6 @@ import {
17
17
  } from '../../revenuecat/presentation/hooks/useSubscriptionQueries';
18
18
  import { usePaywallVisibility } from './usePaywallVisibility';
19
19
 
20
- declare const __DEV__: boolean;
21
-
22
20
  export interface UsePremiumResult {
23
21
  isPremium: boolean;
24
22
  isLoading: boolean;
@@ -14,8 +14,6 @@ import { usePremium } from "./usePremium";
14
14
  import { SubscriptionManager } from "../../revenuecat";
15
15
  import { usePurchaseLoadingStore } from "../stores";
16
16
 
17
- declare const __DEV__: boolean;
18
-
19
17
  export interface UseSavedPurchaseAutoExecutionParams {
20
18
  onSuccess?: () => void;
21
19
  onError?: (error: Error) => void;
@@ -118,15 +116,17 @@ export const useSavedPurchaseAutoExecution = (
118
116
 
119
117
  if (isReady) {
120
118
  const pkg = savedPurchase.pkg;
121
- clearSavedPurchase();
122
119
 
123
120
  startPurchaseRef.current(pkg.product.identifier, "auto-execution");
124
121
 
125
122
  try {
126
123
  const success = await purchasePackageRef.current(pkg);
127
124
 
128
- if (success && onSuccessRef.current) {
129
- onSuccessRef.current();
125
+ if (success) {
126
+ clearSavedPurchase();
127
+ if (onSuccessRef.current) {
128
+ onSuccessRef.current();
129
+ }
130
130
  }
131
131
  } catch (error) {
132
132
  if (onErrorRef.current && error instanceof Error) {
@@ -81,9 +81,9 @@ export const SubscriptionDetailScreen: React.FC<
81
81
  />
82
82
  )}
83
83
 
84
- {showCredits && (
84
+ {showCredits && config.credits && (
85
85
  <CreditsList
86
- credits={config.credits!}
86
+ credits={config.credits}
87
87
  title={
88
88
  config.translations.usageTitle || config.translations.creditsTitle
89
89
  }
@@ -92,11 +92,11 @@ export const SubscriptionDetailScreen: React.FC<
92
92
  />
93
93
  )}
94
94
 
95
- {showUpgradePrompt && (
95
+ {showUpgradePrompt && config.upgradePrompt && (
96
96
  <UpgradePrompt
97
- title={config.upgradePrompt!.title}
98
- subtitle={config.upgradePrompt!.subtitle}
99
- benefits={config.upgradePrompt!.benefits}
97
+ title={config.upgradePrompt.title}
98
+ subtitle={config.upgradePrompt.subtitle}
99
+ benefits={config.upgradePrompt.benefits}
100
100
  upgradeButtonLabel={config.translations.upgradeButton}
101
101
  onUpgrade={config.onUpgrade}
102
102
  />
@@ -6,8 +6,6 @@
6
6
 
7
7
  import { create } from "zustand";
8
8
 
9
- declare const __DEV__: boolean;
10
-
11
9
  export interface PurchaseLoadingState {
12
10
  /** Whether a purchase is in progress */
13
11
  isPurchasing: boolean;
@@ -8,7 +8,7 @@ import type { PurchasesPackage, PurchasesOffering, CustomerInfo } from "react-na
8
8
  export interface InitializeResult {
9
9
  success: boolean;
10
10
  offering: PurchasesOffering | null;
11
- hasPremium: boolean;
11
+ isPremium: boolean;
12
12
  }
13
13
 
14
14
  export interface PurchaseResult {
@@ -7,8 +7,6 @@ import type { PurchasesPackage, CustomerInfo } from "react-native-purchases";
7
7
  import type { IRevenueCatService } from "../../application/ports/IRevenueCatService";
8
8
  import { getPremiumEntitlement } from "../../domain/types/RevenueCatTypes";
9
9
 
10
- declare const __DEV__: boolean;
11
-
12
10
  export interface PremiumStatus {
13
11
  isPremium: boolean;
14
12
  expirationDate: Date | null;
@@ -4,8 +4,6 @@
4
4
  * Coordinates UserIdProvider, InitializationCache, and PackageHandler
5
5
  */
6
6
 
7
- declare const __DEV__: boolean;
8
-
9
7
  import type { PurchasesPackage } from "react-native-purchases";
10
8
  import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
11
9
  import type { IRevenueCatService } from "../../application/ports/IRevenueCatService";
@@ -15,8 +15,6 @@ import {
15
15
  type RenewalState,
16
16
  } from "../utils/RenewalDetector";
17
17
 
18
- declare const __DEV__: boolean;
19
-
20
18
  export class CustomerInfoListenerManager {
21
19
  private listener: CustomerInfoUpdateListener | null = null;
22
20
  private currentUserId: string | null = null;
@@ -6,8 +6,6 @@ import { isUserCancelledError, getErrorMessage } from "../../domain/types/Revenu
6
6
  import { syncPremiumStatus, notifyPurchaseCompleted } from "../utils/PremiumStatusSyncer";
7
7
  import { getSavedPurchase, clearSavedPurchase } from "../../../presentation/hooks/useAuthAwarePurchase";
8
8
 
9
- declare const __DEV__: boolean;
10
-
11
9
  export interface PurchaseHandlerDeps {
12
10
  config: RevenueCatConfig;
13
11
  isInitialized: () => boolean;
@@ -22,7 +22,7 @@ export async function handleRestore(deps: RestoreHandlerDeps, userId: string): P
22
22
  }
23
23
  await notifyRestoreCompleted(deps.config, userId, isPremium, customerInfo);
24
24
 
25
- return { success: isPremium, isPremium, customerInfo };
25
+ return { success: true, isPremium, customerInfo };
26
26
  } catch (error) {
27
27
  throw new RevenueCatRestoreError(getErrorMessage(error, "Restore failed"));
28
28
  }
@@ -3,8 +3,6 @@ import type { InitializeResult } from "../../application/ports/IRevenueCatServic
3
3
  import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
4
4
  import { resolveApiKey } from "../utils/ApiKeyResolver";
5
5
 
6
- declare const __DEV__: boolean;
7
-
8
6
  export interface InitializerDeps {
9
7
  config: RevenueCatConfig;
10
8
  isInitialized: () => boolean;
@@ -33,8 +31,8 @@ function configureLogHandler(): void {
33
31
  }
34
32
 
35
33
  function buildSuccessResult(deps: InitializerDeps, customerInfo: CustomerInfo, offerings: PurchasesOfferings): InitializeResult {
36
- const hasPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
37
- return { success: true, offering: offerings.current, hasPremium };
34
+ const isPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
35
+ return { success: true, offering: offerings.current, isPremium };
38
36
  }
39
37
 
40
38
  export async function initializeSDK(
@@ -50,7 +48,7 @@ export async function initializeSDK(
50
48
  ]);
51
49
  return buildSuccessResult(deps, customerInfo, offerings);
52
50
  } catch {
53
- return { success: false, offering: null, hasPremium: false };
51
+ return { success: false, offering: null, isPremium: false };
54
52
  }
55
53
  }
56
54
 
@@ -69,20 +67,20 @@ export async function initializeSDK(
69
67
  const offerings = await Purchases.getOfferings();
70
68
  return buildSuccessResult(deps, customerInfo, offerings);
71
69
  } catch {
72
- return { success: false, offering: null, hasPremium: false };
70
+ return { success: false, offering: null, isPremium: false };
73
71
  }
74
72
  }
75
73
 
76
74
  if (configurationInProgress) {
77
75
  await new Promise(resolve => setTimeout(resolve, 100));
78
76
  if (isPurchasesConfigured) return initializeSDK(deps, userId, apiKey);
79
- return { success: false, offering: null, hasPremium: false };
77
+ return { success: false, offering: null, isPremium: false };
80
78
  }
81
79
 
82
80
  const key = apiKey || resolveApiKey(deps.config);
83
81
  if (!key) {
84
82
  if (__DEV__) console.log('[RevenueCat] No API key');
85
- return { success: false, offering: null, hasPremium: false };
83
+ return { success: false, offering: null, isPremium: false };
86
84
  }
87
85
 
88
86
  configurationInProgress = true;
@@ -108,7 +106,7 @@ export async function initializeSDK(
108
106
  return buildSuccessResult(deps, customerInfo, offerings);
109
107
  } catch (error) {
110
108
  if (__DEV__) console.error('[RevenueCat] Init failed:', error);
111
- return { success: false, offering: null, hasPremium: false };
109
+ return { success: false, offering: null, isPremium: false };
112
110
  } finally {
113
111
  configurationInProgress = false;
114
112
  }
@@ -57,10 +57,12 @@ export class RevenueCatService implements IRevenueCatService {
57
57
 
58
58
  async initialize(userId: string, apiKey?: string): Promise<InitializeResult> {
59
59
  if (this.isInitialized() && this.getCurrentUserId() === userId) {
60
+ const customerInfo = await Purchases.getCustomerInfo();
61
+ const isPremium = !!customerInfo.entitlements.active[this.stateManager.getConfig().entitlementIdentifier];
60
62
  return {
61
63
  success: true,
62
64
  offering: await this.fetchOfferings(),
63
- hasPremium: false,
65
+ isPremium,
64
66
  };
65
67
  }
66
68
 
@@ -8,8 +8,6 @@ import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConf
8
8
  import type { PurchaseSource } from "../../../domain/entities/Credits";
9
9
  import { getPremiumEntitlement } from "../../domain/types/RevenueCatTypes";
10
10
 
11
- declare const __DEV__: boolean;
12
-
13
11
  export async function syncPremiumStatus(
14
12
  config: RevenueCatConfig,
15
13
  userId: string,
@@ -82,7 +82,15 @@ export function detectRenewal(
82
82
  };
83
83
  }
84
84
 
85
- const newExpiration = new Date(newExpirationDate ?? 0);
85
+ if (!newExpirationDate) {
86
+ // Lifetime subscription (no expiration) - not a renewal
87
+ return {
88
+ ...baseResult,
89
+ productId,
90
+ newExpirationDate,
91
+ };
92
+ }
93
+ const newExpiration = new Date(newExpirationDate);
86
94
  const previousExpiration = new Date(state.previousExpirationDate);
87
95
  const productChanged = productId !== state.previousProductId;
88
96
  const expirationExtended = newExpiration > previousExpiration;
@@ -16,8 +16,6 @@ import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionM
16
16
  import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
17
17
  import { creditsQueryKeys } from "../../../presentation/hooks/useCredits";
18
18
 
19
- declare const __DEV__: boolean;
20
-
21
19
  /** Purchase mutation result - simplified for presentation layer */
22
20
  export interface PurchaseMutationResult {
23
21
  success: boolean;
@@ -11,8 +11,6 @@ import Purchases, {
11
11
  } from "react-native-purchases";
12
12
  import { getRevenueCatService } from "../../infrastructure/services/RevenueCatService";
13
13
 
14
- declare const __DEV__: boolean;
15
-
16
14
  /** Trial eligibility info for a single product */
17
15
  export interface ProductTrialEligibility {
18
16
  /** Product identifier */
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { PremiumStatusFetcher } from './types';
8
+ import { isGuest } from './authUtils';
8
9
 
9
10
  /**
10
11
  * Get isPremium value with centralized logic
@@ -13,17 +14,17 @@ export function getIsPremium(
13
14
  isGuestFlag: boolean,
14
15
  userId: string | null,
15
16
  isPremiumOrFetcher: boolean | PremiumStatusFetcher,
16
- ): boolean | Promise<boolean> {
17
+ ): Promise<boolean> {
17
18
  // Guest users NEVER have premium
18
- if (isGuestFlag || userId === null) return false;
19
+ if (isGuest(isGuestFlag, userId)) return Promise.resolve(false);
19
20
 
20
21
  // Sync mode: return the provided isPremium value
21
- if (typeof isPremiumOrFetcher === 'boolean') return isPremiumOrFetcher;
22
+ if (typeof isPremiumOrFetcher === 'boolean') return Promise.resolve(isPremiumOrFetcher);
22
23
 
23
24
  // Async mode: fetch premium status
24
25
  return (async () => {
25
26
  try {
26
- return await isPremiumOrFetcher.isPremium(userId);
27
+ return await isPremiumOrFetcher.isPremium(userId!);
27
28
  } catch (error) {
28
29
  throw new Error(
29
30
  `Failed to fetch premium status: ${error instanceof Error ? error.message : String(error)}`
@@ -20,6 +20,14 @@ export function formatPrice(price: number, currencyCode: string): string {
20
20
  }
21
21
  }
22
22
 
23
+ import { detectPackageType } from './packageTypeDetector';
24
+
25
+ const PERIOD_SUFFIX_MAP: Record<string, string> = {
26
+ weekly: '/week',
27
+ monthly: '/month',
28
+ yearly: '/year',
29
+ };
30
+
23
31
  /**
24
32
  * Extract billing period suffix from package identifier
25
33
  * Apple App Store Guideline 3.1.2 Compliance:
@@ -30,21 +38,8 @@ export function formatPrice(price: number, currencyCode: string): string {
30
38
  * @returns Billing period suffix (e.g., "/week", "/month", "/year") or empty string
31
39
  */
32
40
  export function getBillingPeriodSuffix(identifier: string): string {
33
- const lowerIdentifier = identifier.toLowerCase();
34
-
35
- if (lowerIdentifier.includes('weekly') || lowerIdentifier.includes('week')) {
36
- return '/week';
37
- }
38
-
39
- if (lowerIdentifier.includes('monthly') || lowerIdentifier.includes('month')) {
40
- return '/month';
41
- }
42
-
43
- if (lowerIdentifier.includes('annual') || lowerIdentifier.includes('year') || lowerIdentifier.includes('yearly')) {
44
- return '/year';
45
- }
46
-
47
- return '';
41
+ const packageType = detectPackageType(identifier);
42
+ return PERIOD_SUFFIX_MAP[packageType] ?? '';
48
43
  }
49
44
 
50
45
  /**
@@ -5,13 +5,14 @@
5
5
  */
6
6
 
7
7
  import type { UserTierInfo } from './types';
8
+ import { isGuest } from './authUtils';
8
9
 
9
10
  export function getUserTierInfo(
10
11
  isGuestFlag: boolean,
11
12
  userId: string | null,
12
13
  isPremium: boolean,
13
14
  ): UserTierInfo {
14
- if (isGuestFlag || userId === null) {
15
+ if (isGuest(isGuestFlag, userId)) {
15
16
  return {
16
17
  tier: 'guest',
17
18
  isPremium: false,
@@ -35,6 +36,6 @@ export function checkPremiumAccess(
35
36
  userId: string | null,
36
37
  isPremium: boolean,
37
38
  ): boolean {
38
- if (isGuestFlag || userId === null) return false;
39
+ if (isGuest(isGuestFlag, userId)) return false;
39
40
  return isPremium;
40
41
  }
@@ -1,79 +0,0 @@
1
- /**
2
- * useSubscription Utilities
3
- * Shared utilities for subscription hook operations
4
- */
5
-
6
- import { getSubscriptionService } from "../../infrastructure/services/SubscriptionService";
7
-
8
- export type AsyncSubscriptionOperation<T> = () => Promise<T>;
9
-
10
- /**
11
- * Result of a subscription service initialization check
12
- */
13
- export interface ServiceCheckResult {
14
- success: boolean;
15
- service: ReturnType<typeof getSubscriptionService> | null;
16
- error?: string;
17
- }
18
-
19
- /**
20
- * Checks if subscription service is initialized
21
- * Returns service instance or error
22
- */
23
- export function checkSubscriptionService(): ServiceCheckResult {
24
- const service = getSubscriptionService();
25
-
26
- if (!service) {
27
- return {
28
- success: false,
29
- service: null,
30
- error: "Subscription service is not initialized",
31
- };
32
- }
33
-
34
- return { success: true, service, error: undefined };
35
- }
36
-
37
- /**
38
- * Validates user ID
39
- */
40
- export function validateUserId(userId: string): string | null {
41
- if (!userId) {
42
- return "User ID is required";
43
- }
44
- return null;
45
- }
46
-
47
- /**
48
- * Wraps async subscription operations with loading state, error handling, and state updates
49
- */
50
- export async function executeSubscriptionOperation<T>(
51
- operation: AsyncSubscriptionOperation<T>,
52
- setLoading: (loading: boolean) => void,
53
- setError: (error: string | null) => void,
54
- onSuccess?: (result: T) => void
55
- ): Promise<void> {
56
- setLoading(true);
57
- setError(null);
58
-
59
- try {
60
- const result = await operation();
61
- if (onSuccess) {
62
- onSuccess(result);
63
- }
64
- } catch (err) {
65
- const errorMessage =
66
- err instanceof Error ? err.message : "Operation failed";
67
- setError(errorMessage);
68
- throw err;
69
- } finally {
70
- setLoading(false);
71
- }
72
- }
73
-
74
- /**
75
- * Formats error message from unknown error
76
- */
77
- export function formatErrorMessage(err: unknown, fallbackMessage: string): string {
78
- return err instanceof Error ? err.message : fallbackMessage;
79
- }