@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
@@ -1,112 +0,0 @@
1
- /**
2
- * Subscription Initializer
3
- *
4
- * Uses RevenueCat best practices:
5
- * - Non-blocking initialization (fire and forget)
6
- * - Relies on CustomerInfoUpdateListener for state updates
7
- */
8
-
9
- import { Platform } from "react-native";
10
- import { configureCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
11
- import { SubscriptionManager } from "../infrastructure/managers/SubscriptionManager";
12
- import { configureAuthProvider } from "../presentation/useAuthAwarePurchase";
13
- import { SubscriptionSyncService } from "./SubscriptionSyncService";
14
- import { getCurrentUserId, setupAuthStateListener } from "./SubscriptionAuthListener";
15
- import type { SubscriptionInitConfig } from "./SubscriptionInitializerTypes";
16
-
17
- export type { FirebaseAuthLike, CreditPackageConfig, SubscriptionInitConfig } from "./SubscriptionInitializerTypes";
18
-
19
- export const initializeSubscription = async (config: SubscriptionInitConfig): Promise<void> => {
20
- const {
21
- apiKey,
22
- apiKeyIos,
23
- apiKeyAndroid,
24
- entitlementId,
25
- credits,
26
- getAnonymousUserId,
27
- getFirebaseAuth,
28
- showAuthModal,
29
- onCreditsUpdated,
30
- creditPackages,
31
- } = config;
32
-
33
- const key = Platform.OS === 'ios'
34
- ? (apiKeyIos || apiKey)
35
- : (apiKeyAndroid || apiKey);
36
-
37
- if (!key) {
38
- throw new Error('API key required');
39
- }
40
-
41
- if (!creditPackages) {
42
- throw new Error('creditPackages is required');
43
- }
44
-
45
- if (!creditPackages.identifierPattern) {
46
- throw new Error('creditPackages.identifierPattern is required');
47
- }
48
-
49
- if (!creditPackages.amounts) {
50
- throw new Error('creditPackages.amounts is required');
51
- }
52
-
53
- if (!getAnonymousUserId) {
54
- throw new Error('getAnonymousUserId is required');
55
- }
56
-
57
- // 1. Configure Repository
58
- configureCreditsRepository({
59
- ...credits,
60
- creditPackageAmounts: creditPackages.amounts
61
- });
62
-
63
- // 2. Setup Sync Service
64
- const syncService = new SubscriptionSyncService(entitlementId);
65
-
66
- // 3. Configure Subscription Manager
67
- SubscriptionManager.configure({
68
- config: {
69
- apiKey: key,
70
- entitlementIdentifier: entitlementId,
71
- consumableProductIdentifiers: [creditPackages.identifierPattern],
72
- onPurchaseCompleted: (u: string, p: string, c: any, s: any) => syncService.handlePurchase(u, p, c, s),
73
- onRenewalDetected: (u: string, p: string, expires: string, c: any) => syncService.handleRenewal(u, p, expires, c),
74
- onPremiumStatusChanged: (u: string, isP: boolean, pId: any, exp: any, willR: any, pt: any) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt),
75
- onCreditsUpdated,
76
- },
77
- apiKey: key,
78
- getAnonymousUserId,
79
- });
80
-
81
- // 4. Configure Auth aware actions
82
- configureAuthProvider({
83
- isAuthenticated: () => {
84
- const auth = getFirebaseAuth();
85
- if (!auth) {
86
- throw new Error("Firebase auth is not available");
87
- }
88
-
89
- const u = auth.currentUser;
90
- return !!(u && !u.isAnonymous);
91
- },
92
- showAuthModal,
93
- });
94
-
95
- const initializeInBackground = async (userId?: string): Promise<void> => {
96
- await SubscriptionManager.initialize(userId);
97
- };
98
-
99
- // 5. Start Background Init
100
- const auth = getFirebaseAuth();
101
- if (!auth) {
102
- throw new Error("Firebase auth is not available");
103
- }
104
-
105
- const initialUserId = getCurrentUserId(() => auth);
106
- await initializeInBackground(initialUserId);
107
-
108
- // 6. Listen for Auth Changes
109
- setupAuthStateListener(() => auth, (newUserId) => {
110
- initializeInBackground(newUserId);
111
- });
112
- };
@@ -1,113 +0,0 @@
1
- /**
2
- * useCustomerInfo Hook
3
- * Fetches and manages RevenueCat CustomerInfo with real-time updates
4
- *
5
- * BEST PRACTICE: Always get expiration date from CustomerInfo (source of truth)
6
- * Never calculate expiration dates client-side (purchaseDate + 1 year is WRONG)
7
- *
8
- * This hook provides:
9
- * - Initial fetch from SDK cache (instant, no network)
10
- * - Real-time listener for updates (renewals, purchases, restore)
11
- * - Automatic cleanup on unmount
12
- * - SDK caches CustomerInfo and fetches every ~5 minutes
13
- *
14
- * @see https://www.revenuecat.com/docs/customers/customer-info
15
- */
16
-
17
- import { useEffect, useState, useCallback, useRef } from "react";
18
- import Purchases, { type CustomerInfo } from "react-native-purchases";
19
-
20
- export interface UseCustomerInfoResult {
21
- /** Current CustomerInfo from RevenueCat SDK */
22
- customerInfo: CustomerInfo | null;
23
- /** Loading state (only true on initial fetch) */
24
- loading: boolean;
25
- /** Error message if fetch failed */
26
- error: string | null;
27
- /** Manually refetch CustomerInfo (usually not needed, listener handles updates) */
28
- refetch: () => Promise<void>;
29
- /** Whether SDK is currently fetching */
30
- isFetching: boolean;
31
- }
32
-
33
- /**
34
- * Hook to get CustomerInfo from RevenueCat SDK
35
- *
36
- * Features:
37
- * - SDK cache: First call returns cached data (instant)
38
- * - Auto-updates: Listener triggers on renewals, purchases, restore
39
- * - Network fetch: SDK fetches every ~5 minutes in background
40
- * - Grace periods: Expiration dates include grace period automatically
41
- *
42
- * @example
43
- * ```typescript
44
- * const { customerInfo, loading } = useCustomerInfo();
45
- *
46
- * // Check premium status
47
- * const isPremium = !!customerInfo?.entitlements.active['premium'];
48
- *
49
- * // Get expiration date (ALWAYS from CustomerInfo, NEVER calculate!)
50
- * const expiresAt = customerInfo?.entitlements.active['premium']?.expirationDate;
51
- *
52
- * // Check will renew
53
- * const willRenew = customerInfo?.entitlements.active['premium']?.willRenew;
54
- * ```
55
- *
56
- * @returns CustomerInfo and loading state
57
- */
58
- export function useCustomerInfo(): UseCustomerInfoResult {
59
- const [customerInfo, setCustomerInfo] = useState<CustomerInfo | null>(null);
60
- const [loading, setLoading] = useState(true);
61
- const [isFetching, setIsFetching] = useState(false);
62
- const [error, setError] = useState<string | null>(null);
63
-
64
- const fetchCustomerInfo = useCallback(async () => {
65
- try {
66
- setIsFetching(true);
67
- setError(null);
68
-
69
- // SDK returns cached data instantly if available
70
- // Network fetch happens in background automatically
71
- const info = await Purchases.getCustomerInfo();
72
-
73
- setCustomerInfo(info);
74
- } catch (err) {
75
- const errorMessage =
76
- err instanceof Error ? err.message : "Failed to fetch customer info";
77
- setError(errorMessage);
78
- } finally {
79
- setLoading(false);
80
- setIsFetching(false);
81
- }
82
- }, []);
83
-
84
- const listenerRef = useRef<((info: CustomerInfo) => void) | null>(null);
85
-
86
- useEffect(() => {
87
- fetchCustomerInfo();
88
-
89
- const listener = (info: CustomerInfo) => {
90
- setCustomerInfo(info);
91
- setError(null);
92
- };
93
-
94
- // Set ref BEFORE adding listener to ensure cleanup can always find it
95
- listenerRef.current = listener;
96
- Purchases.addCustomerInfoUpdateListener(listener);
97
-
98
- return () => {
99
- if (listenerRef.current) {
100
- Purchases.removeCustomerInfoUpdateListener(listenerRef.current);
101
- listenerRef.current = null;
102
- }
103
- };
104
- }, [fetchCustomerInfo]);
105
-
106
- return {
107
- customerInfo,
108
- loading,
109
- error,
110
- refetch: fetchCustomerInfo,
111
- isFetching,
112
- };
113
- }
@@ -1,141 +0,0 @@
1
- /**
2
- * Renewal Detector
3
- * Detects subscription renewals by tracking expiration date changes
4
- * Best Practice: Compare expiration dates to detect renewal events
5
- */
6
-
7
- import type { CustomerInfo } from "react-native-purchases";
8
- import { detectPackageType } from "../../../../utils/packageTypeDetector";
9
-
10
- export interface RenewalState {
11
- previousExpirationDate: string | null;
12
- previousProductId: string | null;
13
- }
14
-
15
- export interface RenewalDetectionResult {
16
- isRenewal: boolean;
17
- isPlanChange: boolean;
18
- isUpgrade: boolean;
19
- isDowngrade: boolean;
20
- productId: string | null;
21
- previousProductId: string | null;
22
- newExpirationDate: string | null;
23
- }
24
-
25
- const PACKAGE_TIER_ORDER: Record<string, number> = {
26
- weekly: 1,
27
- monthly: 2,
28
- yearly: 3,
29
- unknown: 0,
30
- };
31
-
32
- function getPackageTier(productId: string | null): number {
33
- if (!productId) return 0;
34
- const packageType = detectPackageType(productId);
35
- return PACKAGE_TIER_ORDER[packageType] ?? 0;
36
- }
37
-
38
- /**
39
- * Detects if a subscription renewal or plan change occurred
40
- *
41
- * Best Practice (RevenueCat):
42
- * - Track previous expiration date
43
- * - If new expiration > previous → Renewal detected
44
- * - If productId changed → Plan change (upgrade/downgrade)
45
- * - Reset credits on renewal or plan change (industry standard)
46
- *
47
- * @param state Previous state (expiration date, product ID)
48
- * @param customerInfo Current CustomerInfo from RevenueCat
49
- * @param entitlementId Entitlement identifier to check
50
- * @returns Renewal detection result
51
- */
52
- export function detectRenewal(
53
- state: RenewalState,
54
- customerInfo: CustomerInfo,
55
- entitlementId: string
56
- ): RenewalDetectionResult {
57
- const entitlement = customerInfo.entitlements.active[entitlementId];
58
-
59
- const baseResult: RenewalDetectionResult = {
60
- isRenewal: false,
61
- isPlanChange: false,
62
- isUpgrade: false,
63
- isDowngrade: false,
64
- productId: null,
65
- previousProductId: state.previousProductId,
66
- newExpirationDate: null,
67
- };
68
-
69
- if (!entitlement) {
70
- return baseResult;
71
- }
72
-
73
- const newExpirationDate = entitlement.expirationDate;
74
- const productId = entitlement.productIdentifier;
75
-
76
- // First time seeing this subscription - not a renewal
77
- if (!state.previousExpirationDate || !state.previousProductId) {
78
- return {
79
- ...baseResult,
80
- productId,
81
- newExpirationDate,
82
- };
83
- }
84
-
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);
94
- const previousExpiration = new Date(state.previousExpirationDate);
95
- const productChanged = productId !== state.previousProductId;
96
- const expirationExtended = newExpiration > previousExpiration;
97
-
98
- // Plan change detection (upgrade/downgrade)
99
- if (productChanged) {
100
- const oldTier = getPackageTier(state.previousProductId);
101
- const newTier = getPackageTier(productId);
102
- const isUpgrade = newTier > oldTier;
103
- const isDowngrade = newTier < oldTier;
104
-
105
- return {
106
- isRenewal: false,
107
- isPlanChange: true,
108
- isUpgrade,
109
- isDowngrade,
110
- productId,
111
- previousProductId: state.previousProductId,
112
- newExpirationDate,
113
- };
114
- }
115
-
116
- // Same product renewal
117
- const isRenewal = expirationExtended;
118
-
119
- return {
120
- isRenewal,
121
- isPlanChange: false,
122
- isUpgrade: false,
123
- isDowngrade: false,
124
- productId,
125
- previousProductId: state.previousProductId,
126
- newExpirationDate,
127
- };
128
- }
129
-
130
- /**
131
- * Updates renewal state after detection
132
- */
133
- export function updateRenewalState(
134
- _state: RenewalState,
135
- result: RenewalDetectionResult
136
- ): RenewalState {
137
- return {
138
- previousExpirationDate: result.newExpirationDate,
139
- previousProductId: result.productId,
140
- };
141
- }
@@ -1,114 +0,0 @@
1
- /**
2
- * Transaction Repository
3
- *
4
- * Firestore operations for credit transaction logs.
5
- * Generic repository for use across hundreds of apps.
6
- */
7
-
8
- import {
9
- getDocs,
10
- addDoc,
11
- query,
12
- where,
13
- orderBy,
14
- limit as firestoreLimit,
15
- serverTimestamp,
16
- type QueryConstraint,
17
- } from "firebase/firestore";
18
- import { BaseRepository } from "@umituz/react-native-firebase";
19
- import type {
20
- CreditLog,
21
- TransactionRepositoryConfig,
22
- TransactionQueryOptions,
23
- TransactionResult,
24
- TransactionReason,
25
- } from "../../domain/types/transaction.types";
26
- import { TransactionMapper } from "../../domain/mappers/TransactionMapper";
27
- import { requireFirestore, buildCollectionRef, type CollectionConfig, mapErrorToResult } from "../../../../shared/infrastructure/firestore";
28
-
29
- export class TransactionRepository extends BaseRepository {
30
- private config: TransactionRepositoryConfig;
31
-
32
- constructor(config: TransactionRepositoryConfig) {
33
- super(config.collectionName);
34
- this.config = config;
35
- }
36
-
37
- private getCollectionConfig(): CollectionConfig {
38
- return {
39
- collectionName: this.config.collectionName,
40
- useUserSubcollection: this.config.useUserSubcollection ?? false,
41
- };
42
- }
43
-
44
- private getCollectionRef(db: any, userId: string) {
45
- const config = this.getCollectionConfig();
46
- return buildCollectionRef(db, userId, config);
47
- }
48
-
49
- async getTransactions(
50
- options: TransactionQueryOptions
51
- ): Promise<TransactionResult> {
52
- try {
53
- const db = requireFirestore();
54
- const colRef = this.getCollectionRef(db, options.userId);
55
- const constraints: QueryConstraint[] = [];
56
-
57
- if (!this.config.useUserSubcollection) {
58
- constraints.push(where("userId", "==", options.userId));
59
- }
60
-
61
- constraints.push(orderBy("createdAt", "desc"));
62
- constraints.push(firestoreLimit(options.limit ?? 50));
63
-
64
- const q = query(colRef, ...constraints);
65
- const snapshot = await getDocs(q);
66
-
67
- const transactions: CreditLog[] = snapshot.docs.map((docSnap) =>
68
- TransactionMapper.toEntity(docSnap, options.userId)
69
- );
70
-
71
- return { success: true, data: transactions };
72
- } catch (error) {
73
- return mapErrorToResult(error);
74
- }
75
- }
76
-
77
- async addTransaction(
78
- userId: string,
79
- change: number,
80
- reason: TransactionReason,
81
- metadata?: Partial<CreditLog>
82
- ): Promise<TransactionResult<CreditLog>> {
83
- try {
84
- const db = requireFirestore();
85
- const colRef = this.getCollectionRef(db, userId);
86
- const docData = {
87
- ...TransactionMapper.toFirestore(userId, change, reason, metadata),
88
- createdAt: serverTimestamp(),
89
- };
90
-
91
- const docRef = await addDoc(colRef, docData);
92
-
93
- return {
94
- success: true,
95
- data: {
96
- id: docRef.id,
97
- userId,
98
- change,
99
- reason,
100
- ...metadata,
101
- createdAt: Date.now(),
102
- },
103
- };
104
- } catch (error) {
105
- return mapErrorToResult<CreditLog>(error);
106
- }
107
- }
108
- }
109
-
110
- export function createTransactionRepository(
111
- config: TransactionRepositoryConfig
112
- ): TransactionRepository {
113
- return new TransactionRepository(config);
114
- }
@@ -1,114 +0,0 @@
1
- /**
2
- * Product Metadata Service
3
- *
4
- * Generic service for fetching product metadata from Firestore.
5
- * Collection name is configurable for use across hundreds of apps.
6
- */
7
-
8
- import { collection, getDocs, orderBy, query } from "firebase/firestore";
9
- import { requireFirestore } from "../../../../shared/infrastructure";
10
- import type {
11
- ProductMetadata,
12
- ProductMetadataConfig,
13
- ProductType,
14
- } from "../../domain/types/wallet.types";
15
-
16
- interface CacheEntry {
17
- data: ProductMetadata[];
18
- timestamp: number;
19
- }
20
-
21
- const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
22
-
23
- export class ProductMetadataService {
24
- private config: ProductMetadataConfig;
25
- private cache: CacheEntry | null = null;
26
-
27
- constructor(config: ProductMetadataConfig) {
28
- this.config = config;
29
- }
30
-
31
- private isCacheValid(): boolean {
32
- if (!this.cache) return false;
33
- const ttl = this.config.cacheTTL ?? DEFAULT_CACHE_TTL_MS;
34
- return Date.now() - this.cache.timestamp < ttl;
35
- }
36
-
37
- private async fetchFromFirebase(): Promise<ProductMetadata[]> {
38
- const db = requireFirestore();
39
- const colRef = collection(db, this.config.collectionName);
40
- const q = query(colRef, orderBy("order", "asc"));
41
- const snapshot = await getDocs(q);
42
-
43
- return snapshot.docs.map((docSnap) => ({
44
- productId: docSnap.id,
45
- ...docSnap.data(),
46
- })) as ProductMetadata[];
47
- }
48
-
49
- async getAll(): Promise<ProductMetadata[]> {
50
- if (this.isCacheValid() && this.cache) {
51
- return this.cache.data;
52
- }
53
-
54
- try {
55
- const data = await this.fetchFromFirebase();
56
- this.cache = { data, timestamp: Date.now() };
57
- return data;
58
- } catch (error) {
59
- if (this.cache) {
60
- return this.cache.data;
61
- }
62
- throw error;
63
- }
64
- }
65
-
66
- async getByProductId(productId: string): Promise<ProductMetadata | null> {
67
- const all = await this.getAll();
68
- return all.find((p) => p.productId === productId) ?? null;
69
- }
70
-
71
- async getByType(type: ProductType): Promise<ProductMetadata[]> {
72
- const all = await this.getAll();
73
- return all.filter((p) => p.type === type);
74
- }
75
-
76
- async getCreditsPackages(): Promise<ProductMetadata[]> {
77
- return this.getByType("credits");
78
- }
79
-
80
- async getSubscriptionPackages(): Promise<ProductMetadata[]> {
81
- return this.getByType("subscription");
82
- }
83
-
84
- clearCache(): void {
85
- this.cache = null;
86
- }
87
- }
88
-
89
- export function createProductMetadataService(
90
- config: ProductMetadataConfig
91
- ): ProductMetadataService {
92
- return new ProductMetadataService(config);
93
- }
94
-
95
- let defaultService: ProductMetadataService | null = null;
96
-
97
- export function configureProductMetadataService(
98
- config: ProductMetadataConfig
99
- ): void {
100
- defaultService = new ProductMetadataService(config);
101
- }
102
-
103
- export function getProductMetadataService(): ProductMetadataService {
104
- if (!defaultService) {
105
- throw new Error(
106
- "ProductMetadataService not configured. Call configureProductMetadataService first."
107
- );
108
- }
109
- return defaultService;
110
- }
111
-
112
- export function resetProductMetadataService(): void {
113
- defaultService = null;
114
- }