@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.
- package/package.json +1 -1
- package/src/domains/credits/infrastructure/operations/CreditsFetcher.ts +1 -2
- package/src/domains/credits/infrastructure/operations/CreditsInitializer.ts +1 -1
- package/src/domains/credits/presentation/deduct-credit/index.ts +2 -0
- package/src/domains/credits/presentation/deduct-credit/mutationConfig.ts +81 -0
- package/src/domains/credits/presentation/deduct-credit/types.ts +11 -0
- package/src/domains/credits/presentation/deduct-credit/useDeductCredit.ts +44 -0
- package/src/domains/subscription/application/initializer/BackgroundInitializer.ts +21 -0
- package/src/domains/subscription/application/initializer/ConfigValidator.ts +33 -0
- package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +45 -0
- package/src/domains/subscription/application/initializer/SubscriptionInitializer.ts +11 -0
- package/src/domains/subscription/application/initializer/index.ts +2 -0
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +13 -94
- package/src/domains/subscription/infrastructure/handlers/package-operations/PackageFetcher.ts +57 -0
- package/src/domains/subscription/infrastructure/handlers/package-operations/PackagePurchaser.ts +15 -0
- package/src/domains/subscription/infrastructure/handlers/package-operations/PackageRestorer.ts +34 -0
- package/src/domains/subscription/infrastructure/handlers/package-operations/PremiumStatusChecker.ts +9 -0
- package/src/domains/subscription/infrastructure/handlers/package-operations/index.ts +5 -0
- package/src/domains/subscription/infrastructure/handlers/package-operations/types.ts +4 -0
- package/src/domains/subscription/infrastructure/hooks/customer-info/index.ts +2 -0
- package/src/domains/subscription/infrastructure/hooks/customer-info/types.ts +9 -0
- package/src/domains/subscription/infrastructure/hooks/customer-info/useCustomerInfo.ts +57 -0
- package/src/domains/subscription/infrastructure/services/listeners/CustomerInfoHandler.ts +1 -1
- package/src/domains/subscription/infrastructure/services/listeners/ListenerState.ts +1 -1
- package/src/domains/subscription/infrastructure/services/purchase/PurchaseErrorHandler.ts +0 -1
- package/src/domains/subscription/infrastructure/utils/renewal/PackageTierComparator.ts +14 -0
- package/src/domains/subscription/infrastructure/utils/renewal/RenewalDetector.ts +78 -0
- package/src/domains/subscription/infrastructure/utils/renewal/RenewalStateUpdater.ts +11 -0
- package/src/domains/subscription/infrastructure/utils/renewal/index.ts +3 -0
- package/src/domains/subscription/infrastructure/utils/renewal/types.ts +14 -0
- package/src/domains/wallet/index.ts +2 -2
- package/src/domains/wallet/infrastructure/repositories/transaction/CollectionBuilder.ts +14 -0
- package/src/domains/wallet/infrastructure/repositories/transaction/TransactionFetcher.ts +46 -0
- package/src/domains/wallet/infrastructure/repositories/transaction/TransactionRepository.ts +34 -0
- package/src/domains/wallet/infrastructure/repositories/transaction/TransactionWriter.ts +43 -0
- package/src/domains/wallet/infrastructure/repositories/transaction/index.ts +10 -0
- package/src/domains/wallet/infrastructure/services/product-metadata/CacheManager.ts +30 -0
- package/src/domains/wallet/infrastructure/services/product-metadata/FirebaseFetcher.ts +17 -0
- package/src/domains/wallet/infrastructure/services/product-metadata/ProductMetadataService.ts +57 -0
- package/src/domains/wallet/infrastructure/services/product-metadata/ServiceManager.ts +29 -0
- package/src/domains/wallet/infrastructure/services/product-metadata/index.ts +7 -0
- package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +1 -1
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +1 -1
- package/src/index.ts +2 -2
- package/src/init/createSubscriptionInitModule.ts +1 -1
- package/src/domains/credits/presentation/useDeductCredit.ts +0 -110
- package/src/domains/subscription/application/SubscriptionInitializer.ts +0 -112
- package/src/domains/subscription/infrastructure/hooks/useCustomerInfo.ts +0 -113
- package/src/domains/subscription/infrastructure/utils/RenewalDetector.ts +0 -141
- package/src/domains/wallet/infrastructure/repositories/TransactionRepository.ts +0 -114
- package/src/domains/wallet/infrastructure/services/ProductMetadataService.ts +0 -114
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { CustomerInfo } from "react-native-purchases";
|
|
2
|
+
import type { RenewalState, RenewalDetectionResult } from "./types";
|
|
3
|
+
import { getPackageTier } from "./PackageTierComparator";
|
|
4
|
+
|
|
5
|
+
export function detectRenewal(
|
|
6
|
+
state: RenewalState,
|
|
7
|
+
customerInfo: CustomerInfo,
|
|
8
|
+
entitlementId: string
|
|
9
|
+
): RenewalDetectionResult {
|
|
10
|
+
const entitlement = customerInfo.entitlements.active[entitlementId];
|
|
11
|
+
|
|
12
|
+
const baseResult: RenewalDetectionResult = {
|
|
13
|
+
isRenewal: false,
|
|
14
|
+
isPlanChange: false,
|
|
15
|
+
isUpgrade: false,
|
|
16
|
+
isDowngrade: false,
|
|
17
|
+
productId: null,
|
|
18
|
+
previousProductId: state.previousProductId,
|
|
19
|
+
newExpirationDate: null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (!entitlement) {
|
|
23
|
+
return baseResult;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const newExpirationDate = entitlement.expirationDate;
|
|
27
|
+
const productId = entitlement.productIdentifier;
|
|
28
|
+
|
|
29
|
+
if (!state.previousExpirationDate || !state.previousProductId) {
|
|
30
|
+
return {
|
|
31
|
+
...baseResult,
|
|
32
|
+
productId,
|
|
33
|
+
newExpirationDate,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!newExpirationDate) {
|
|
38
|
+
return {
|
|
39
|
+
...baseResult,
|
|
40
|
+
productId,
|
|
41
|
+
newExpirationDate,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const newExpiration = new Date(newExpirationDate);
|
|
46
|
+
const previousExpiration = new Date(state.previousExpirationDate);
|
|
47
|
+
const productChanged = productId !== state.previousProductId;
|
|
48
|
+
const expirationExtended = newExpiration > previousExpiration;
|
|
49
|
+
|
|
50
|
+
if (productChanged) {
|
|
51
|
+
const oldTier = getPackageTier(state.previousProductId);
|
|
52
|
+
const newTier = getPackageTier(productId);
|
|
53
|
+
const isUpgrade = newTier > oldTier;
|
|
54
|
+
const isDowngrade = newTier < oldTier;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
isRenewal: false,
|
|
58
|
+
isPlanChange: true,
|
|
59
|
+
isUpgrade,
|
|
60
|
+
isDowngrade,
|
|
61
|
+
productId,
|
|
62
|
+
previousProductId: state.previousProductId,
|
|
63
|
+
newExpirationDate,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const isRenewal = expirationExtended;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
isRenewal,
|
|
71
|
+
isPlanChange: false,
|
|
72
|
+
isUpgrade: false,
|
|
73
|
+
isDowngrade: false,
|
|
74
|
+
productId,
|
|
75
|
+
previousProductId: state.previousProductId,
|
|
76
|
+
newExpirationDate,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RenewalState, RenewalDetectionResult } from "./types";
|
|
2
|
+
|
|
3
|
+
export function updateRenewalState(
|
|
4
|
+
_state: RenewalState,
|
|
5
|
+
result: RenewalDetectionResult
|
|
6
|
+
): RenewalState {
|
|
7
|
+
return {
|
|
8
|
+
previousExpirationDate: result.newExpirationDate,
|
|
9
|
+
previousProductId: result.productId,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface RenewalState {
|
|
2
|
+
previousExpirationDate: string | null;
|
|
3
|
+
previousProductId: string | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface RenewalDetectionResult {
|
|
7
|
+
isRenewal: boolean;
|
|
8
|
+
isPlanChange: boolean;
|
|
9
|
+
isUpgrade: boolean;
|
|
10
|
+
isDowngrade: boolean;
|
|
11
|
+
productId: string | null;
|
|
12
|
+
previousProductId: string | null;
|
|
13
|
+
newExpirationDate: string | null;
|
|
14
|
+
}
|
|
@@ -64,7 +64,7 @@ export {
|
|
|
64
64
|
export {
|
|
65
65
|
TransactionRepository,
|
|
66
66
|
createTransactionRepository,
|
|
67
|
-
} from "./infrastructure/repositories/
|
|
67
|
+
} from "./infrastructure/repositories/transaction";
|
|
68
68
|
|
|
69
69
|
// Services
|
|
70
70
|
export {
|
|
@@ -73,7 +73,7 @@ export {
|
|
|
73
73
|
configureProductMetadataService,
|
|
74
74
|
getProductMetadataService,
|
|
75
75
|
resetProductMetadataService,
|
|
76
|
-
} from "./infrastructure/services/
|
|
76
|
+
} from "./infrastructure/services/product-metadata";
|
|
77
77
|
|
|
78
78
|
// Hooks
|
|
79
79
|
export {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { buildCollectionRef, type CollectionConfig } from "../../../../../shared/infrastructure/firestore";
|
|
2
|
+
import type { TransactionRepositoryConfig } from "../../../domain/types/transaction.types";
|
|
3
|
+
|
|
4
|
+
export function getCollectionConfig(config: TransactionRepositoryConfig): CollectionConfig {
|
|
5
|
+
return {
|
|
6
|
+
collectionName: config.collectionName,
|
|
7
|
+
useUserSubcollection: config.useUserSubcollection ?? false,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getCollectionRef(db: any, userId: string, config: TransactionRepositoryConfig) {
|
|
12
|
+
const collectionConfig = getCollectionConfig(config);
|
|
13
|
+
return buildCollectionRef(db, userId, collectionConfig);
|
|
14
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDocs,
|
|
3
|
+
query,
|
|
4
|
+
where,
|
|
5
|
+
orderBy,
|
|
6
|
+
limit as firestoreLimit,
|
|
7
|
+
type QueryConstraint,
|
|
8
|
+
} from "firebase/firestore";
|
|
9
|
+
import type {
|
|
10
|
+
CreditLog,
|
|
11
|
+
TransactionRepositoryConfig,
|
|
12
|
+
TransactionQueryOptions,
|
|
13
|
+
TransactionResult,
|
|
14
|
+
} from "../../../domain/types/transaction.types";
|
|
15
|
+
import { TransactionMapper } from "../../../domain/mappers/TransactionMapper";
|
|
16
|
+
import { requireFirestore, mapErrorToResult } from "../../../../../shared/infrastructure/firestore";
|
|
17
|
+
import { getCollectionRef } from "./CollectionBuilder";
|
|
18
|
+
|
|
19
|
+
export async function fetchTransactions(
|
|
20
|
+
config: TransactionRepositoryConfig,
|
|
21
|
+
options: TransactionQueryOptions
|
|
22
|
+
): Promise<TransactionResult> {
|
|
23
|
+
try {
|
|
24
|
+
const db = requireFirestore();
|
|
25
|
+
const colRef = getCollectionRef(db, options.userId, config);
|
|
26
|
+
const constraints: QueryConstraint[] = [];
|
|
27
|
+
|
|
28
|
+
if (!config.useUserSubcollection) {
|
|
29
|
+
constraints.push(where("userId", "==", options.userId));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
constraints.push(orderBy("createdAt", "desc"));
|
|
33
|
+
constraints.push(firestoreLimit(options.limit ?? 50));
|
|
34
|
+
|
|
35
|
+
const q = query(colRef, ...constraints);
|
|
36
|
+
const snapshot = await getDocs(q);
|
|
37
|
+
|
|
38
|
+
const transactions: CreditLog[] = snapshot.docs.map((docSnap) =>
|
|
39
|
+
TransactionMapper.toEntity(docSnap, options.userId)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return { success: true, data: transactions };
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return mapErrorToResult(error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { BaseRepository } from "@umituz/react-native-firebase";
|
|
2
|
+
import type {
|
|
3
|
+
CreditLog,
|
|
4
|
+
TransactionRepositoryConfig,
|
|
5
|
+
TransactionQueryOptions,
|
|
6
|
+
TransactionResult,
|
|
7
|
+
TransactionReason,
|
|
8
|
+
} from "../../../domain/types/transaction.types";
|
|
9
|
+
import { fetchTransactions } from "./TransactionFetcher";
|
|
10
|
+
import { addTransaction as addTransactionOp } from "./TransactionWriter";
|
|
11
|
+
|
|
12
|
+
export class TransactionRepository extends BaseRepository {
|
|
13
|
+
private config: TransactionRepositoryConfig;
|
|
14
|
+
|
|
15
|
+
constructor(config: TransactionRepositoryConfig) {
|
|
16
|
+
super(config.collectionName);
|
|
17
|
+
this.config = config;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async getTransactions(
|
|
21
|
+
options: TransactionQueryOptions
|
|
22
|
+
): Promise<TransactionResult> {
|
|
23
|
+
return fetchTransactions(this.config, options);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async addTransaction(
|
|
27
|
+
userId: string,
|
|
28
|
+
change: number,
|
|
29
|
+
reason: TransactionReason,
|
|
30
|
+
metadata?: Partial<CreditLog>
|
|
31
|
+
): Promise<TransactionResult<CreditLog>> {
|
|
32
|
+
return addTransactionOp(this.config, userId, change, reason, metadata);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { addDoc, serverTimestamp } from "firebase/firestore";
|
|
2
|
+
import type {
|
|
3
|
+
CreditLog,
|
|
4
|
+
TransactionRepositoryConfig,
|
|
5
|
+
TransactionResult,
|
|
6
|
+
TransactionReason,
|
|
7
|
+
} from "../../../domain/types/transaction.types";
|
|
8
|
+
import { TransactionMapper } from "../../../domain/mappers/TransactionMapper";
|
|
9
|
+
import { requireFirestore, mapErrorToResult } from "../../../../../shared/infrastructure/firestore";
|
|
10
|
+
import { getCollectionRef } from "./CollectionBuilder";
|
|
11
|
+
|
|
12
|
+
export async function addTransaction(
|
|
13
|
+
config: TransactionRepositoryConfig,
|
|
14
|
+
userId: string,
|
|
15
|
+
change: number,
|
|
16
|
+
reason: TransactionReason,
|
|
17
|
+
metadata?: Partial<CreditLog>
|
|
18
|
+
): Promise<TransactionResult<CreditLog>> {
|
|
19
|
+
try {
|
|
20
|
+
const db = requireFirestore();
|
|
21
|
+
const colRef = getCollectionRef(db, userId, config);
|
|
22
|
+
const docData = {
|
|
23
|
+
...TransactionMapper.toFirestore(userId, change, reason, metadata),
|
|
24
|
+
createdAt: serverTimestamp(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const docRef = await addDoc(colRef, docData);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
data: {
|
|
32
|
+
id: docRef.id,
|
|
33
|
+
userId,
|
|
34
|
+
change,
|
|
35
|
+
reason,
|
|
36
|
+
...metadata,
|
|
37
|
+
createdAt: Date.now(),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return mapErrorToResult<CreditLog>(error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TransactionRepositoryConfig } from "../../../domain/types/transaction.types";
|
|
2
|
+
import { TransactionRepository } from "./TransactionRepository";
|
|
3
|
+
|
|
4
|
+
export { TransactionRepository } from "./TransactionRepository";
|
|
5
|
+
|
|
6
|
+
export function createTransactionRepository(
|
|
7
|
+
config: TransactionRepositoryConfig
|
|
8
|
+
): TransactionRepository {
|
|
9
|
+
return new TransactionRepository(config);
|
|
10
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ProductMetadata } from "../../../domain/types/wallet.types";
|
|
2
|
+
|
|
3
|
+
interface CacheEntry {
|
|
4
|
+
data: ProductMetadata[];
|
|
5
|
+
timestamp: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
export class CacheManager {
|
|
11
|
+
private cache: CacheEntry | null = null;
|
|
12
|
+
|
|
13
|
+
isCacheValid(cacheTTL?: number): boolean {
|
|
14
|
+
if (!this.cache) return false;
|
|
15
|
+
const ttl = cacheTTL ?? DEFAULT_CACHE_TTL_MS;
|
|
16
|
+
return Date.now() - this.cache.timestamp < ttl;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get(): ProductMetadata[] | null {
|
|
20
|
+
return this.cache?.data ?? null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
set(data: ProductMetadata[]): void {
|
|
24
|
+
this.cache = { data, timestamp: Date.now() };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
clear(): void {
|
|
28
|
+
this.cache = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { collection, getDocs, orderBy, query } from "firebase/firestore";
|
|
2
|
+
import { requireFirestore } from "../../../../../shared/infrastructure";
|
|
3
|
+
import type { ProductMetadata } from "../../../domain/types/wallet.types";
|
|
4
|
+
|
|
5
|
+
export async function fetchProductsFromFirebase(
|
|
6
|
+
collectionName: string
|
|
7
|
+
): Promise<ProductMetadata[]> {
|
|
8
|
+
const db = requireFirestore();
|
|
9
|
+
const colRef = collection(db, collectionName);
|
|
10
|
+
const q = query(colRef, orderBy("order", "asc"));
|
|
11
|
+
const snapshot = await getDocs(q);
|
|
12
|
+
|
|
13
|
+
return snapshot.docs.map((docSnap) => ({
|
|
14
|
+
productId: docSnap.id,
|
|
15
|
+
...docSnap.data(),
|
|
16
|
+
})) as ProductMetadata[];
|
|
17
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProductMetadata,
|
|
3
|
+
ProductMetadataConfig,
|
|
4
|
+
ProductType,
|
|
5
|
+
} from "../../../domain/types/wallet.types";
|
|
6
|
+
import { CacheManager } from "./CacheManager";
|
|
7
|
+
import { fetchProductsFromFirebase } from "./FirebaseFetcher";
|
|
8
|
+
|
|
9
|
+
export class ProductMetadataService {
|
|
10
|
+
private config: ProductMetadataConfig;
|
|
11
|
+
private cacheManager: CacheManager;
|
|
12
|
+
|
|
13
|
+
constructor(config: ProductMetadataConfig) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.cacheManager = new CacheManager();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async getAll(): Promise<ProductMetadata[]> {
|
|
19
|
+
if (this.cacheManager.isCacheValid(this.config.cacheTTL)) {
|
|
20
|
+
return this.cacheManager.get()!;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const data = await fetchProductsFromFirebase(this.config.collectionName);
|
|
25
|
+
this.cacheManager.set(data);
|
|
26
|
+
return data;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const cachedData = this.cacheManager.get();
|
|
29
|
+
if (cachedData) {
|
|
30
|
+
return cachedData;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getByProductId(productId: string): Promise<ProductMetadata | null> {
|
|
37
|
+
const all = await this.getAll();
|
|
38
|
+
return all.find((p) => p.productId === productId) ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async getByType(type: ProductType): Promise<ProductMetadata[]> {
|
|
42
|
+
const all = await this.getAll();
|
|
43
|
+
return all.filter((p) => p.type === type);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async getCreditsPackages(): Promise<ProductMetadata[]> {
|
|
47
|
+
return this.getByType("credits");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getSubscriptionPackages(): Promise<ProductMetadata[]> {
|
|
51
|
+
return this.getByType("subscription");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
clearCache(): void {
|
|
55
|
+
this.cacheManager.clear();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ProductMetadataConfig } from "../../../domain/types/wallet.types";
|
|
2
|
+
import { ProductMetadataService } from "./ProductMetadataService";
|
|
3
|
+
|
|
4
|
+
export function createProductMetadataService(
|
|
5
|
+
config: ProductMetadataConfig
|
|
6
|
+
): ProductMetadataService {
|
|
7
|
+
return new ProductMetadataService(config);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let defaultService: ProductMetadataService | null = null;
|
|
11
|
+
|
|
12
|
+
export function configureProductMetadataService(
|
|
13
|
+
config: ProductMetadataConfig
|
|
14
|
+
): void {
|
|
15
|
+
defaultService = new ProductMetadataService(config);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getProductMetadataService(): ProductMetadataService {
|
|
19
|
+
if (!defaultService) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"ProductMetadataService not configured. Call configureProductMetadataService first."
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return defaultService;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resetProductMetadataService(): void {
|
|
28
|
+
defaultService = null;
|
|
29
|
+
}
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
ProductMetadataConfig,
|
|
6
6
|
ProductType,
|
|
7
7
|
} from "../../domain/types/wallet.types";
|
|
8
|
-
import { ProductMetadataService } from "../../infrastructure/services/
|
|
8
|
+
import { ProductMetadataService } from "../../infrastructure/services/product-metadata";
|
|
9
9
|
|
|
10
10
|
export const productMetadataQueryKeys = {
|
|
11
11
|
all: ["productMetadata"] as const,
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
CreditLog,
|
|
6
6
|
TransactionRepositoryConfig,
|
|
7
7
|
} from "../../domain/types/transaction.types";
|
|
8
|
-
import { TransactionRepository } from "../../infrastructure/repositories/
|
|
8
|
+
import { TransactionRepository } from "../../infrastructure/repositories/transaction";
|
|
9
9
|
|
|
10
10
|
export const transactionQueryKeys = {
|
|
11
11
|
all: ["transactions"] as const,
|
package/src/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ export {
|
|
|
32
32
|
export type { Result, Success, Failure } from "./shared/utils/Result";
|
|
33
33
|
|
|
34
34
|
// Infrastructure Layer (Services & Repositories)
|
|
35
|
-
export { initializeSubscription, type SubscriptionInitConfig, type CreditPackageConfig } from "./domains/subscription/application/
|
|
35
|
+
export { initializeSubscription, type SubscriptionInitConfig, type CreditPackageConfig } from "./domains/subscription/application/initializer";
|
|
36
36
|
export {
|
|
37
37
|
getDeviceId,
|
|
38
38
|
checkTrialEligibility,
|
|
@@ -53,7 +53,7 @@ export {
|
|
|
53
53
|
// Presentation Layer - Hooks
|
|
54
54
|
export { useAuthAwarePurchase } from "./domains/subscription/presentation/useAuthAwarePurchase";
|
|
55
55
|
export { useCredits } from "./domains/credits/presentation/useCredits";
|
|
56
|
-
export { useDeductCredit } from "./domains/credits/presentation/
|
|
56
|
+
export { useDeductCredit } from "./domains/credits/presentation/deduct-credit";
|
|
57
57
|
export { useFeatureGate } from "./domains/subscription/presentation/useFeatureGate";
|
|
58
58
|
export { usePaywallVisibility, paywallControl } from "./domains/subscription/presentation/usePaywallVisibility";
|
|
59
59
|
export { usePremium } from "./domains/subscription/presentation/usePremium";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { InitModule } from '@umituz/react-native-design-system';
|
|
2
|
-
import { initializeSubscription, type SubscriptionInitConfig } from '../domains/subscription/application/
|
|
2
|
+
import { initializeSubscription, type SubscriptionInitConfig } from '../domains/subscription/application/initializer';
|
|
3
3
|
|
|
4
4
|
export interface SubscriptionInitModuleConfig extends Omit<SubscriptionInitConfig, 'apiKey'> {
|
|
5
5
|
getApiKey: () => string | undefined;
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useDeductCredit Hook
|
|
3
|
-
* TanStack Query mutation hook for deducting credits.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useCallback } from "react";
|
|
7
|
-
import { useMutation, useQueryClient } from "@umituz/react-native-design-system";
|
|
8
|
-
import type { UserCredits } from "../core/Credits";
|
|
9
|
-
import { getCreditsRepository } from "../infrastructure/CreditsRepositoryManager";
|
|
10
|
-
import { creditsQueryKeys } from "./creditsQueryKeys";
|
|
11
|
-
import { calculateRemaining } from "../../../shared/utils/numberUtils";
|
|
12
|
-
|
|
13
|
-
import { timezoneService } from "@umituz/react-native-design-system";
|
|
14
|
-
|
|
15
|
-
export interface UseDeductCreditParams {
|
|
16
|
-
userId: string | undefined;
|
|
17
|
-
onCreditsExhausted?: () => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface UseDeductCreditResult {
|
|
21
|
-
/** Check if user has enough credits (server-side validation) */
|
|
22
|
-
checkCredits: (cost?: number) => Promise<boolean>;
|
|
23
|
-
deductCredit: (cost?: number) => Promise<boolean>;
|
|
24
|
-
deductCredits: (cost: number) => Promise<boolean>;
|
|
25
|
-
isDeducting: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const useDeductCredit = ({
|
|
29
|
-
userId,
|
|
30
|
-
onCreditsExhausted,
|
|
31
|
-
}: UseDeductCreditParams): UseDeductCreditResult => {
|
|
32
|
-
const repository = getCreditsRepository();
|
|
33
|
-
const queryClient = useQueryClient();
|
|
34
|
-
|
|
35
|
-
const mutation = useMutation({
|
|
36
|
-
mutationFn: async (cost: number) => {
|
|
37
|
-
if (!userId) throw new Error("User not authenticated");
|
|
38
|
-
return repository.deductCredit(userId, cost);
|
|
39
|
-
},
|
|
40
|
-
onMutate: async (cost: number) => {
|
|
41
|
-
if (!userId) return { previousCredits: null, skippedOptimistic: true, capturedUserId: null };
|
|
42
|
-
|
|
43
|
-
// Capture userId at mutation start to prevent cross-user contamination
|
|
44
|
-
const capturedUserId = userId;
|
|
45
|
-
|
|
46
|
-
await queryClient.cancelQueries({ queryKey: creditsQueryKeys.user(capturedUserId) });
|
|
47
|
-
const previousCredits = queryClient.getQueryData<UserCredits>(creditsQueryKeys.user(capturedUserId));
|
|
48
|
-
|
|
49
|
-
if (!previousCredits) {
|
|
50
|
-
return { previousCredits: null as UserCredits | null, skippedOptimistic: true, capturedUserId };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Calculate new credits using utility
|
|
54
|
-
const newCredits = calculateRemaining(previousCredits.credits, cost);
|
|
55
|
-
|
|
56
|
-
queryClient.setQueryData<UserCredits | null>(creditsQueryKeys.user(capturedUserId), (old) => {
|
|
57
|
-
if (!old) return old;
|
|
58
|
-
return {
|
|
59
|
-
...old,
|
|
60
|
-
credits: newCredits,
|
|
61
|
-
lastUpdatedAt: timezoneService.getNow()
|
|
62
|
-
};
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
previousCredits,
|
|
67
|
-
skippedOptimistic: false,
|
|
68
|
-
wasInsufficient: previousCredits.credits < cost,
|
|
69
|
-
capturedUserId
|
|
70
|
-
};
|
|
71
|
-
},
|
|
72
|
-
onError: (_err, _cost, mutationData) => {
|
|
73
|
-
// Always restore previous credits on error to prevent UI desync
|
|
74
|
-
// Use captured userId to prevent rollback on wrong user
|
|
75
|
-
if (mutationData?.capturedUserId && mutationData?.previousCredits && !mutationData.skippedOptimistic) {
|
|
76
|
-
queryClient.setQueryData(creditsQueryKeys.user(mutationData.capturedUserId), mutationData.previousCredits);
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
onSuccess: (_data, _cost, mutationData) => {
|
|
80
|
-
const targetUserId = mutationData?.capturedUserId ?? userId;
|
|
81
|
-
if (targetUserId) {
|
|
82
|
-
queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(targetUserId) });
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const deductCredit = useCallback(async (cost: number = 1): Promise<boolean> => {
|
|
88
|
-
try {
|
|
89
|
-
const res = await mutation.mutateAsync(cost);
|
|
90
|
-
if (!res.success) {
|
|
91
|
-
if (res.error?.code === "CREDITS_EXHAUSTED") onCreditsExhausted?.();
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
return true;
|
|
95
|
-
} catch {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
}, [mutation, onCreditsExhausted]);
|
|
99
|
-
|
|
100
|
-
const deductCredits = useCallback(async (cost: number): Promise<boolean> => {
|
|
101
|
-
return await deductCredit(cost);
|
|
102
|
-
}, [deductCredit]);
|
|
103
|
-
|
|
104
|
-
const checkCredits = useCallback(async (cost: number = 1): Promise<boolean> => {
|
|
105
|
-
if (!userId) return false;
|
|
106
|
-
return repository.hasCredits(userId, cost);
|
|
107
|
-
}, [userId, repository]);
|
|
108
|
-
|
|
109
|
-
return { checkCredits, deductCredit, deductCredits, isDeducting: mutation.isPending };
|
|
110
|
-
};
|