@umituz/react-native-subscription 2.25.0 → 2.26.0

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.25.0",
3
+ "version": "2.26.0",
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",
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { SubscriptionPackageType } from "../../utils/packageTypeDetector";
9
+ import type { SubscriptionStatusType, PeriodType } from "./SubscriptionStatus";
9
10
 
10
11
  export type CreditType = "text" | "image";
11
12
 
@@ -19,16 +20,11 @@ export type PurchaseSource =
19
20
 
20
21
  export type PurchaseType = "initial" | "renewal" | "upgrade" | "downgrade";
21
22
 
22
- export type SubscriptionStatus = "active" | "trial" | "trial_canceled" | "expired" | "canceled" | "free";
23
-
24
- /** RevenueCat period types */
25
- export type PeriodType = "NORMAL" | "INTRO" | "TRIAL";
26
-
27
23
  /** Single Source of Truth for user subscription + credits data */
28
24
  export interface UserCredits {
29
25
  // Core subscription
30
26
  isPremium: boolean;
31
- status: SubscriptionStatus;
27
+ status: SubscriptionStatusType;
32
28
 
33
29
  // Dates
34
30
  purchasedAt: Date | null;
@@ -9,8 +9,14 @@ export const SUBSCRIPTION_STATUS = {
9
9
  NONE: 'none',
10
10
  } as const;
11
11
 
12
- /** RevenueCat period types */
13
- export type PeriodType = "NORMAL" | "INTRO" | "TRIAL";
12
+ /** RevenueCat period type constants */
13
+ export const PERIOD_TYPE = {
14
+ NORMAL: 'NORMAL',
15
+ INTRO: 'INTRO',
16
+ TRIAL: 'TRIAL',
17
+ } as const;
18
+
19
+ export type PeriodType = (typeof PERIOD_TYPE)[keyof typeof PERIOD_TYPE];
14
20
 
15
21
  export type SubscriptionStatusType = (typeof SUBSCRIPTION_STATUS)[keyof typeof SUBSCRIPTION_STATUS];
16
22
 
@@ -35,7 +41,7 @@ export const createDefaultSubscriptionStatus = (): SubscriptionStatus => ({
35
41
  purchasedAt: null,
36
42
  customerId: null,
37
43
  syncedAt: null,
38
- status: 'none',
44
+ status: SUBSCRIPTION_STATUS.NONE,
39
45
  });
40
46
 
41
47
  export const isSubscriptionValid = (status: SubscriptionStatus | null): boolean => {
@@ -50,3 +56,32 @@ export const calculateDaysRemaining = (expiresAt: string | null): number | null
50
56
  return timezoneService.getDaysUntil(new Date(expiresAt));
51
57
  };
52
58
 
59
+ /** Subscription status resolver input */
60
+ export interface StatusResolverInput {
61
+ isPremium: boolean;
62
+ willRenew?: boolean;
63
+ isExpired?: boolean;
64
+ periodType?: PeriodType;
65
+ }
66
+
67
+ /**
68
+ * Resolves subscription status from input parameters
69
+ * Single source of truth for status determination logic
70
+ */
71
+ export const resolveSubscriptionStatus = (input: StatusResolverInput): SubscriptionStatusType => {
72
+ const { isPremium, willRenew, isExpired, periodType } = input;
73
+
74
+ if (!isPremium || isExpired) {
75
+ return isExpired ? SUBSCRIPTION_STATUS.EXPIRED : SUBSCRIPTION_STATUS.NONE;
76
+ }
77
+
78
+ const isTrial = periodType === PERIOD_TYPE.TRIAL;
79
+ const isCanceled = willRenew === false;
80
+
81
+ if (isTrial) {
82
+ return isCanceled ? SUBSCRIPTION_STATUS.TRIAL_CANCELED : SUBSCRIPTION_STATUS.TRIAL;
83
+ }
84
+
85
+ return isCanceled ? SUBSCRIPTION_STATUS.CANCELED : SUBSCRIPTION_STATUS.ACTIVE;
86
+ };
87
+
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Result Pattern
3
+ * Type-safe error handling without exceptions.
4
+ * Inspired by Rust's Result<T, E> and functional programming patterns.
5
+ */
6
+
7
+ /** Success result containing data */
8
+ export interface Success<T> {
9
+ readonly success: true;
10
+ readonly data: T;
11
+ readonly error?: never;
12
+ }
13
+
14
+ /** Failure result containing error */
15
+ export interface Failure<E = Error> {
16
+ readonly success: false;
17
+ readonly data?: never;
18
+ readonly error: E;
19
+ }
20
+
21
+ /** Union type representing either success or failure */
22
+ export type Result<T, E = Error> = Success<T> | Failure<E>;
23
+
24
+ /** Create a success result */
25
+ export function success<T>(data: T): Success<T> {
26
+ return { success: true, data };
27
+ }
28
+
29
+ /** Create a failure result */
30
+ export function failure<E = Error>(error: E): Failure<E> {
31
+ return { success: false, error };
32
+ }
33
+
34
+ /** Type guard to check if result is success */
35
+ export function isSuccess<T, E>(result: Result<T, E>): result is Success<T> {
36
+ return result.success === true;
37
+ }
38
+
39
+ /** Type guard to check if result is failure */
40
+ export function isFailure<T, E>(result: Result<T, E>): result is Failure<E> {
41
+ return result.success === false;
42
+ }
43
+
44
+ /** Unwrap result or throw if failure */
45
+ export function unwrap<T, E extends Error>(result: Result<T, E>): T {
46
+ if (isSuccess(result)) {
47
+ return result.data;
48
+ }
49
+ throw result.error;
50
+ }
51
+
52
+ /** Unwrap result or return default value */
53
+ export function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {
54
+ if (isSuccess(result)) {
55
+ return result.data;
56
+ }
57
+ return defaultValue;
58
+ }
59
+
60
+ /** Map success value to new value */
61
+ export function map<T, U, E>(
62
+ result: Result<T, E>,
63
+ fn: (data: T) => U
64
+ ): Result<U, E> {
65
+ if (isSuccess(result)) {
66
+ return success(fn(result.data));
67
+ }
68
+ return result;
69
+ }
70
+
71
+ /** Chain results (flatMap) */
72
+ export function flatMap<T, U, E>(
73
+ result: Result<T, E>,
74
+ fn: (data: T) => Result<U, E>
75
+ ): Result<U, E> {
76
+ if (isSuccess(result)) {
77
+ return fn(result.data);
78
+ }
79
+ return result;
80
+ }
81
+
82
+ /** Execute async function and wrap result */
83
+ export async function tryCatch<T>(
84
+ fn: () => Promise<T>,
85
+ errorMapper?: (error: unknown) => Error
86
+ ): Promise<Result<T, Error>> {
87
+ try {
88
+ const data = await fn();
89
+ return success(data);
90
+ } catch (error) {
91
+ const mappedError = errorMapper
92
+ ? errorMapper(error)
93
+ : error instanceof Error
94
+ ? error
95
+ : new Error(String(error));
96
+ return failure(mappedError);
97
+ }
98
+ }
99
+
100
+ /** Execute sync function and wrap result */
101
+ export function tryCatchSync<T>(
102
+ fn: () => T,
103
+ errorMapper?: (error: unknown) => Error
104
+ ): Result<T, Error> {
105
+ try {
106
+ const data = fn();
107
+ return success(data);
108
+ } catch (error) {
109
+ const mappedError = errorMapper
110
+ ? errorMapper(error)
111
+ : error instanceof Error
112
+ ? error
113
+ : new Error(String(error));
114
+ return failure(mappedError);
115
+ }
116
+ }
117
+
118
+ /** Combine multiple results into one */
119
+ export function combine<T, E>(results: Result<T, E>[]): Result<T[], E> {
120
+ const data: T[] = [];
121
+ for (const result of results) {
122
+ if (isFailure(result)) {
123
+ return result;
124
+ }
125
+ data.push(result.data);
126
+ }
127
+ return success(data);
128
+ }
package/src/index.ts CHANGED
@@ -7,11 +7,33 @@ export * from "./domains/paywall";
7
7
  export * from "./domains/config";
8
8
 
9
9
  // Domain Layer
10
- export { createDefaultSubscriptionStatus, isSubscriptionValid } from "./domain/entities/SubscriptionStatus";
11
- export type { SubscriptionStatus, SubscriptionStatusType, PeriodType } from "./domain/entities/SubscriptionStatus";
10
+ export {
11
+ SUBSCRIPTION_STATUS,
12
+ PERIOD_TYPE,
13
+ createDefaultSubscriptionStatus,
14
+ isSubscriptionValid,
15
+ resolveSubscriptionStatus,
16
+ } from "./domain/entities/SubscriptionStatus";
17
+ export type { SubscriptionStatus, SubscriptionStatusType, PeriodType, StatusResolverInput } from "./domain/entities/SubscriptionStatus";
12
18
  export type { SubscriptionConfig } from "./domain/value-objects/SubscriptionConfig";
13
19
  export type { ISubscriptionRepository } from "./application/ports/ISubscriptionRepository";
14
20
 
21
+ // Result Pattern (Functional Error Handling)
22
+ export {
23
+ success,
24
+ failure,
25
+ isSuccess,
26
+ isFailure,
27
+ unwrap,
28
+ unwrapOr,
29
+ map,
30
+ flatMap,
31
+ tryCatch,
32
+ tryCatchSync,
33
+ combine,
34
+ } from "./domain/value-objects/Result";
35
+ export type { Result, Success, Failure } from "./domain/value-objects/Result";
36
+
15
37
  // Infrastructure Layer
16
38
  export { SubscriptionService, initializeSubscriptionService } from "./infrastructure/services/SubscriptionService";
17
39
  export {
@@ -1,4 +1,5 @@
1
- import type { UserCredits, SubscriptionStatus, PeriodType } from "../../domain/entities/Credits";
1
+ import type { UserCredits } from "../../domain/entities/Credits";
2
+ import { resolveSubscriptionStatus, type PeriodType, type SubscriptionStatusType } from "../../domain/entities/SubscriptionStatus";
2
3
  import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
3
4
 
4
5
  /** Maps Firestore document to domain entity with expiration validation */
@@ -51,43 +52,22 @@ export class CreditsMapper {
51
52
  doc: UserCreditsDocumentRead,
52
53
  expirationDate: Date | null,
53
54
  periodType?: PeriodType
54
- ): { isPremium: boolean; status: SubscriptionStatus } {
55
- const docIsPremium = doc.isPremium ?? false;
55
+ ): { isPremium: boolean; status: SubscriptionStatusType } {
56
+ const isPremium = doc.isPremium ?? false;
56
57
  const willRenew = doc.willRenew ?? false;
58
+ const isExpired = expirationDate ? expirationDate < new Date() : false;
57
59
 
58
- // No expiration date = lifetime or free
59
- if (!expirationDate) {
60
- return {
61
- isPremium: docIsPremium,
62
- status: docIsPremium ? "active" : "free",
63
- };
64
- }
65
-
66
- // Check if subscription has expired
67
- const isExpired = expirationDate < new Date();
68
-
69
- if (isExpired) {
70
- // Subscription expired - override document's isPremium
71
- return { isPremium: false, status: "expired" };
72
- }
73
-
74
- // Handle trial period
75
- if (periodType === "TRIAL") {
76
- return {
77
- isPremium: docIsPremium,
78
- status: willRenew === false ? "trial_canceled" : "trial",
79
- };
80
- }
81
-
82
- // Handle canceled subscription (will not renew but still active)
83
- if (docIsPremium && willRenew === false) {
84
- return { isPremium: true, status: "canceled" };
85
- }
60
+ const status = resolveSubscriptionStatus({
61
+ isPremium,
62
+ willRenew,
63
+ isExpired,
64
+ periodType,
65
+ });
86
66
 
87
- // Subscription still active
67
+ // Override isPremium if expired
88
68
  return {
89
- isPremium: docIsPremium,
90
- status: docIsPremium ? "active" : "free",
69
+ isPremium: isExpired ? false : isPremium,
70
+ status,
91
71
  };
92
72
  }
93
73
  }
@@ -15,9 +15,8 @@ import type {
15
15
  PurchaseSource,
16
16
  PurchaseType,
17
17
  PurchaseMetadata,
18
- SubscriptionDocStatus,
19
- PeriodType,
20
18
  } from "../models/UserCreditsDocument";
19
+ import { SUBSCRIPTION_STATUS, resolveSubscriptionStatus, type PeriodType } from "../../domain/entities/SubscriptionStatus";
21
20
  import { TRIAL_CONFIG } from "./TrialService";
22
21
  import { detectPackageType } from "../../utils/packageTypeDetector";
23
22
  import { getCreditAllocation } from "../../utils/creditMapper";
@@ -126,34 +125,23 @@ export async function initializeCreditsTransaction(
126
125
  ? [...(existing?.purchaseHistory || []), purchaseMetadata].slice(-10)
127
126
  : existing?.purchaseHistory;
128
127
 
129
- // Determine subscription status based on isPremium, willRenew, and periodType
128
+ // Determine subscription status
130
129
  const isPremium = metadata?.isPremium ?? true;
131
130
  const willRenew = metadata?.willRenew;
132
131
  const periodType = metadata?.periodType;
133
- const isTrialing = periodType === "TRIAL";
134
-
135
- // Status logic:
136
- // - trial: periodType is TRIAL and premium
137
- // - trial_canceled: periodType is TRIAL and premium but willRenew=false
138
- // - canceled: premium but willRenew=false (non-trial)
139
- // - expired: not premium
140
- // - active: premium and will renew (non-trial)
141
- let status: SubscriptionDocStatus;
142
- if (!isPremium) {
143
- status = "expired";
144
- } else if (isTrialing) {
145
- status = willRenew === false ? "trial_canceled" : "trial";
146
- } else if (willRenew === false) {
147
- status = "canceled";
148
- } else {
149
- status = "active";
150
- }
132
+
133
+ const status = resolveSubscriptionStatus({
134
+ isPremium,
135
+ willRenew,
136
+ isExpired: !isPremium,
137
+ periodType,
138
+ });
151
139
 
152
140
  // Determine credits based on status
153
141
  // Trial: 5 credits, Trial canceled: 0 credits, Normal: plan-based credits
154
- if (status === "trial") {
142
+ if (status === SUBSCRIPTION_STATUS.TRIAL) {
155
143
  newCredits = TRIAL_CONFIG.CREDITS;
156
- } else if (status === "trial_canceled") {
144
+ } else if (status === SUBSCRIPTION_STATUS.TRIAL_CANCELED) {
157
145
  newCredits = 0;
158
146
  }
159
147
 
@@ -198,11 +186,13 @@ export async function initializeCreditsTransaction(
198
186
  }
199
187
 
200
188
  // Trial-specific fields
189
+ const isTrialing = status === SUBSCRIPTION_STATUS.TRIAL || status === SUBSCRIPTION_STATUS.TRIAL_CANCELED;
190
+
201
191
  if (periodType) {
202
192
  creditsData.periodType = periodType;
203
193
  }
204
194
  if (isTrialing) {
205
- creditsData.isTrialing = true;
195
+ creditsData.isTrialing = status === SUBSCRIPTION_STATUS.TRIAL;
206
196
  creditsData.trialCredits = TRIAL_CONFIG.CREDITS;
207
197
  // Set trial dates if this is a new trial
208
198
  if (!existing?.trialStartDate) {
@@ -211,7 +201,7 @@ export async function initializeCreditsTransaction(
211
201
  if (metadata?.expirationDate) {
212
202
  creditsData.trialEndDate = Timestamp.fromDate(new Date(metadata.expirationDate));
213
203
  }
214
- } else if (existing?.isTrialing && !isTrialing && isPremium) {
204
+ } else if (existing?.isTrialing && isPremium) {
215
205
  // User converted from trial to paid
216
206
  creditsData.isTrialing = false;
217
207
  creditsData.convertedFromTrial = true;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Subscription Logger
3
+ * Centralized logging utility for the subscription package.
4
+ * All logs are dev-only and automatically stripped in production.
5
+ */
6
+
7
+ declare const __DEV__: boolean;
8
+
9
+ export type LogLevel = "debug" | "info" | "warn" | "error";
10
+
11
+ export interface LogContext {
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ /** Log categories for filtering and grouping */
16
+ export const LOG_CATEGORY = {
17
+ REVENUECAT: "RevenueCat",
18
+ CREDITS: "Credits",
19
+ TRIAL: "Trial",
20
+ PURCHASE: "Purchase",
21
+ FEATURE_GATE: "FeatureGate",
22
+ SUBSCRIPTION: "Subscription",
23
+ SYNC: "Sync",
24
+ } as const;
25
+
26
+ export type LogCategory = (typeof LOG_CATEGORY)[keyof typeof LOG_CATEGORY];
27
+
28
+ /**
29
+ * Development-only logger that automatically respects __DEV__ flag.
30
+ * All methods are no-ops in production builds.
31
+ */
32
+ class SubscriptionLogger {
33
+ private enabled: boolean = true;
34
+ private categories: Set<LogCategory> = new Set();
35
+
36
+ /** Enable or disable all logging */
37
+ setEnabled(enabled: boolean): void {
38
+ this.enabled = enabled;
39
+ }
40
+
41
+ /** Enable logging for specific categories only */
42
+ setCategories(categories: LogCategory[]): void {
43
+ this.categories = new Set(categories);
44
+ }
45
+
46
+ /** Clear category filter (log all categories) */
47
+ clearCategories(): void {
48
+ this.categories.clear();
49
+ }
50
+
51
+ private shouldLog(category?: LogCategory): boolean {
52
+ if (typeof __DEV__ === "undefined" || !__DEV__) return false;
53
+ if (!this.enabled) return false;
54
+ if (this.categories.size > 0 && category && !this.categories.has(category)) return false;
55
+ return true;
56
+ }
57
+
58
+ private formatMessage(category: LogCategory | string, message: string): string {
59
+ return `[${category}] ${message}`;
60
+ }
61
+
62
+ debug(category: LogCategory, message: string, context?: LogContext): void {
63
+ if (!this.shouldLog(category)) return;
64
+ console.log(this.formatMessage(category, message), context ?? "");
65
+ }
66
+
67
+ info(category: LogCategory, message: string, context?: LogContext): void {
68
+ if (!this.shouldLog(category)) return;
69
+ console.log(this.formatMessage(category, message), context ?? "");
70
+ }
71
+
72
+ warn(category: LogCategory, message: string, context?: LogContext): void {
73
+ if (!this.shouldLog(category)) return;
74
+ console.warn(this.formatMessage(category, message), context ?? "");
75
+ }
76
+
77
+ error(category: LogCategory, message: string, error?: unknown, context?: LogContext): void {
78
+ if (!this.shouldLog(category)) return;
79
+ console.error(this.formatMessage(category, message), { error, ...context });
80
+ }
81
+
82
+ /** Log purchase flow events */
83
+ purchase(message: string, context?: LogContext): void {
84
+ this.debug(LOG_CATEGORY.PURCHASE, message, context);
85
+ }
86
+
87
+ /** Log credits-related events */
88
+ credits(message: string, context?: LogContext): void {
89
+ this.debug(LOG_CATEGORY.CREDITS, message, context);
90
+ }
91
+
92
+ /** Log trial-related events */
93
+ trial(message: string, context?: LogContext): void {
94
+ this.debug(LOG_CATEGORY.TRIAL, message, context);
95
+ }
96
+
97
+ /** Log RevenueCat SDK events */
98
+ revenueCat(message: string, context?: LogContext): void {
99
+ this.debug(LOG_CATEGORY.REVENUECAT, message, context);
100
+ }
101
+
102
+ /** Log feature gate events */
103
+ featureGate(message: string, context?: LogContext): void {
104
+ this.debug(LOG_CATEGORY.FEATURE_GATE, message, context);
105
+ }
106
+
107
+ /** Log sync operations */
108
+ sync(message: string, context?: LogContext): void {
109
+ this.debug(LOG_CATEGORY.SYNC, message, context);
110
+ }
111
+ }
112
+
113
+ /** Singleton logger instance */
114
+ export const Logger = new SubscriptionLogger();
@@ -14,6 +14,8 @@ import {
14
14
  isCreditsRepositoryConfigured,
15
15
  } from "../../infrastructure/repositories/CreditsRepositoryProvider";
16
16
 
17
+ declare const __DEV__: boolean;
18
+
17
19
  export const creditsQueryKeys = {
18
20
  all: ["credits"] as const,
19
21
  user: (userId: string) => ["credits", userId] as const,
@@ -77,7 +79,11 @@ export const useCredits = ({
77
79
 
78
80
  // Background sync: If mapper detected expired status, sync to Firestore
79
81
  if (result.data?.status === "expired") {
80
- repository.syncExpiredStatus(userId).catch(() => {});
82
+ repository.syncExpiredStatus(userId).catch((syncError) => {
83
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
84
+ console.warn("[useCredits] Background sync failed:", syncError);
85
+ }
86
+ });
81
87
  }
82
88
 
83
89
  return result.data || null;
@@ -173,10 +173,8 @@ export function useFeatureGate(
173
173
  authGate,
174
174
  creditsGate,
175
175
  hasSubscription,
176
- hasCredits,
177
176
  requiredCredits,
178
177
  onShowAuthModal,
179
- onShowPaywall,
180
178
  ]
181
179
  );
182
180
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { useMemo } from "react";
7
7
  import type { UserCredits } from "../../domain/entities/Credits";
8
+ import { resolveSubscriptionStatus, type PeriodType, type SubscriptionStatusType } from "../../domain/entities/SubscriptionStatus";
8
9
  import type { SubscriptionSettingsTranslations } from "../types/SubscriptionSettingsTypes";
9
10
 
10
11
  export interface CreditsInfo {
@@ -38,42 +39,19 @@ export function useCreditsArray(
38
39
 
39
40
  /**
40
41
  * Calculates subscription status type based on premium, renewal status, and period type
41
- * @param isPremium - Whether user has premium subscription
42
- * @param willRenew - Whether subscription will auto-renew (false = canceled)
43
- * @param expiresAt - Expiration date ISO string (null for lifetime)
44
- * @param periodType - RevenueCat period type: NORMAL, INTRO, or TRIAL
45
42
  */
46
43
  export function getSubscriptionStatusType(
47
44
  isPremium: boolean,
48
45
  willRenew?: boolean,
49
46
  expiresAt?: string | null,
50
- periodType?: "NORMAL" | "INTRO" | "TRIAL"
51
- ): "active" | "trial" | "trial_canceled" | "canceled" | "expired" | "none" {
52
- if (!isPremium) {
53
- return "none";
54
- }
55
-
56
- // Lifetime subscription (no expiration) - always active
57
- if (!expiresAt) {
58
- return "active";
59
- }
60
-
61
- // Check if expired
62
- const now = new Date();
63
- const expDate = new Date(expiresAt);
64
- if (expDate < now) {
65
- return "expired";
66
- }
67
-
68
- // Trial period handling
69
- if (periodType === "TRIAL") {
70
- return willRenew === false ? "trial_canceled" : "trial";
71
- }
72
-
73
- // Premium with willRenew=false means subscription is canceled but still active until expiration
74
- if (willRenew === false) {
75
- return "canceled";
76
- }
77
-
78
- return "active";
47
+ periodType?: PeriodType
48
+ ): SubscriptionStatusType {
49
+ const isExpired = expiresAt ? new Date(expiresAt) < new Date() : false;
50
+
51
+ return resolveSubscriptionStatus({
52
+ isPremium,
53
+ willRenew,
54
+ isExpired,
55
+ periodType,
56
+ });
79
57
  }
@@ -13,7 +13,8 @@ import { creditsQueryKeys } from "../../../presentation/hooks/useCredits";
13
13
 
14
14
  declare const __DEV__: boolean;
15
15
 
16
- export interface PurchaseResult {
16
+ /** Purchase mutation result - simplified for presentation layer */
17
+ export interface PurchaseMutationResult {
17
18
  success: boolean;
18
19
  productId: string;
19
20
  }
@@ -28,7 +29,7 @@ export const usePurchasePackage = (userId: string | undefined) => {
28
29
  const { showSuccess, showError } = useAlert();
29
30
 
30
31
  return useMutation({
31
- mutationFn: async (pkg: PurchasesPackage): Promise<PurchaseResult> => {
32
+ mutationFn: async (pkg: PurchasesPackage): Promise<PurchaseMutationResult> => {
32
33
  if (!userId) {
33
34
  throw new Error("User not authenticated");
34
35
  }
@@ -10,3 +10,7 @@ export * from "./priceUtils";
10
10
  export * from "./tierUtils";
11
11
  export * from "./types";
12
12
  export * from "./validation";
13
+
14
+ // Logger (infrastructure utility re-exported for convenience)
15
+ export { Logger, LOG_CATEGORY } from "../infrastructure/utils/Logger";
16
+ export type { LogLevel, LogCategory, LogContext } from "../infrastructure/utils/Logger";