@umituz/react-native-subscription 2.27.65 → 2.27.67
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 +3 -1
- package/src/domains/credits/application/CreditLimitCalculator.ts +17 -0
- package/src/domains/credits/application/CreditsInitializer.ts +85 -0
- package/src/domains/credits/application/DeductCreditsCommand.ts +52 -0
- package/src/domains/credits/application/PurchaseMetadataGenerator.ts +59 -0
- package/src/domains/credits/application/credit-strategies/CreditAllocationContext.ts +35 -0
- package/src/domains/credits/application/credit-strategies/ICreditStrategy.ts +18 -0
- package/src/domains/credits/application/credit-strategies/StandardPurchaseCreditStrategy.ts +16 -0
- package/src/domains/credits/application/credit-strategies/SyncCreditStrategy.ts +15 -0
- package/src/domains/credits/application/credit-strategies/TrialCreditStrategy.ts +18 -0
- package/src/{infrastructure/mappers → domains/credits/core}/CreditsMapper.ts +4 -4
- package/src/domains/credits/infrastructure/CreditsRepository.ts +102 -0
- package/src/{presentation/hooks → domains/credits/presentation}/useCredits.ts +21 -4
- package/src/domains/subscription/application/SubscriptionAuthListener.ts +26 -0
- package/src/domains/subscription/application/SubscriptionInitializer.ts +77 -0
- package/src/{infrastructure/services → domains/subscription/application}/SubscriptionInitializerTypes.ts +21 -1
- package/src/domains/subscription/application/SubscriptionSyncService.ts +71 -0
- package/src/domains/subscription/application/SubscriptionSyncUtils.ts +16 -0
- package/src/{revenuecat/domain/value-objects → domains/subscription/core}/RevenueCatConfig.ts +1 -1
- package/src/{domain/types → domains/subscription/core}/RevenueCatData.ts +1 -1
- package/src/{domain/entities → domains/subscription/core}/SubscriptionStatus.ts +13 -21
- package/src/domains/subscription/core/SubscriptionStatusHandlers.ts +51 -0
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +67 -0
- package/src/domains/subscription/infrastructure/handlers/PurchaseStatusResolver.ts +27 -0
- package/src/domains/subscription/infrastructure/managers/SubscriptionInternalState.ts +12 -0
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +110 -0
- package/src/{presentation/hooks → domains/subscription/presentation}/usePremium.ts +7 -4
- package/src/domains/trial/application/TrialEligibilityService.ts +25 -0
- package/src/domains/trial/application/TrialService.ts +68 -0
- package/src/{infrastructure/services → domains/trial/core}/TrialTypes.ts +1 -1
- package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +30 -0
- package/src/presentation/components/details/PremiumStatusBadge.tsx +2 -2
- package/src/presentation/hooks/index.ts +11 -11
- package/src/shared/infrastructure/SubscriptionEventBus.ts +51 -0
- package/src/utils/packageTypeDetector.ts +13 -18
- package/src/application/README.md +0 -50
- package/src/domain/entities/README.md +0 -50
- package/src/domain/entities/SubscriptionStatus.test.ts +0 -105
- package/src/domain/errors/README.md +0 -53
- package/src/domain/value-objects/README.md +0 -50
- package/src/infrastructure/README.md +0 -55
- package/src/infrastructure/mappers/README.md +0 -21
- package/src/infrastructure/models/README.md +0 -26
- package/src/infrastructure/repositories/CreditsRepository.ts +0 -132
- package/src/infrastructure/repositories/README.md +0 -99
- package/src/infrastructure/services/CreditsInitializer.ts +0 -170
- package/src/infrastructure/services/README.md +0 -99
- package/src/infrastructure/services/SubscriptionInitializer.ts +0 -176
- package/src/infrastructure/services/SubscriptionService.ts +0 -133
- package/src/infrastructure/services/TrialService.ts +0 -197
- package/src/infrastructure/services/app-service-helpers.ts +0 -111
- package/src/revenuecat/README.md +0 -104
- package/src/revenuecat/application/README.md +0 -43
- package/src/revenuecat/application/ports/IRevenueCatService.ts +0 -76
- package/src/revenuecat/application/ports/README.md +0 -41
- package/src/revenuecat/domain/README.md +0 -48
- package/src/revenuecat/domain/constants/README.md +0 -41
- package/src/revenuecat/domain/entities/README.md +0 -42
- package/src/revenuecat/domain/errors/README.md +0 -53
- package/src/revenuecat/domain/types/README.md +0 -41
- package/src/revenuecat/domain/value-objects/README.md +0 -41
- package/src/revenuecat/index.ts +0 -13
- package/src/revenuecat/infrastructure/handlers/PackageHandler.ts +0 -161
- package/src/revenuecat/infrastructure/managers/SubscriptionManager.ts +0 -165
- package/src/revenuecat/presentation/README.md +0 -42
- /package/src/{domain/entities → domains/credits/core}/Credits.ts +0 -0
- /package/src/{infrastructure/models → domains/credits/core}/UserCreditsDocument.ts +0 -0
- /package/src/{infrastructure/repositories → domains/credits/infrastructure}/CreditsRepositoryProvider.ts +0 -0
- /package/src/{presentation/hooks → domains/credits/presentation}/useDeductCredit.ts +0 -0
- /package/src/{revenuecat/domain/constants → domains/subscription/core}/RevenueCatConstants.ts +0 -0
- /package/src/{revenuecat/domain/errors → domains/subscription/core}/RevenueCatError.ts +0 -0
- /package/src/{revenuecat/domain/types → domains/subscription/core}/RevenueCatTypes.ts +0 -0
- /package/src/{domain/entities → domains/subscription/core}/SubscriptionConstants.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/README.md +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/config/README.md +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/handlers/README.md +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/README.md +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/subscriptionQueryKeys.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useCustomerInfo.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useInitializeSubscription.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/usePaywallFlow.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/usePurchasePackage.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRestorePurchase.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRevenueCat.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useRevenueCatTrialEligibility.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useSubscriptionPackages.ts +0 -0
- /package/src/{revenuecat/presentation → domains/subscription/infrastructure}/hooks/useSubscriptionQueries.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/managers/README.md +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/CustomerInfoListenerManager.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/OfferingsFetcher.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/PurchaseHandler.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/README.md +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/RestoreHandler.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/RevenueCatInitializer.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/RevenueCatService.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/services/ServiceStateManager.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/utils/ApiKeyResolver.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/utils/InitializationCache.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/utils/PremiumStatusSyncer.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/utils/README.md +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/utils/RenewalDetector.ts +0 -0
- /package/src/{revenuecat → domains/subscription}/infrastructure/utils/UserIdProvider.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/useAuthAwarePurchase.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/useAuthSubscriptionSync.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/useFeatureGate.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/usePaywallVisibility.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/usePremiumGate.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/useSavedPurchaseAutoExecution.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionSettingsConfig.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionSettingsConfig.utils.ts +0 -0
- /package/src/{presentation/hooks → domains/subscription/presentation}/useSubscriptionStatus.ts +0 -0
- /package/src/{infrastructure/services → shared/application}/ActivationHandler.ts +0 -0
- /package/src/{infrastructure/services → shared/application}/FeedbackService.ts +0 -0
- /package/src/{application → shared/application}/ports/ISubscriptionRepository.ts +0 -0
- /package/src/{application → shared/application}/ports/ISubscriptionService.ts +0 -0
- /package/src/{application → shared/application}/ports/README.md +0 -0
- /package/src/{domain/errors → shared/utils}/InsufficientCreditsError.ts +0 -0
- /package/src/{infrastructure → shared}/utils/Logger.ts +0 -0
- /package/src/{domain/value-objects → shared/utils}/Result.ts +0 -0
- /package/src/{domain/value-objects → shared/utils}/SubscriptionConfig.ts +0 -0
- /package/src/{domain/errors → shared/utils}/SubscriptionError.ts +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
type EventCallback<T = any> = (data: T) => void;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple EventBus Implementation
|
|
5
|
+
* Used to decouple services and provide an observer pattern for subscription events.
|
|
6
|
+
*/
|
|
7
|
+
export class SubscriptionEventBus {
|
|
8
|
+
private static instance: SubscriptionEventBus;
|
|
9
|
+
private listeners: Record<string, EventCallback[]> = {};
|
|
10
|
+
|
|
11
|
+
private constructor() {}
|
|
12
|
+
|
|
13
|
+
static getInstance(): SubscriptionEventBus {
|
|
14
|
+
if (!SubscriptionEventBus.instance) {
|
|
15
|
+
SubscriptionEventBus.instance = new SubscriptionEventBus();
|
|
16
|
+
}
|
|
17
|
+
return SubscriptionEventBus.instance;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
on<T>(event: string, callback: EventCallback<T>): () => void {
|
|
21
|
+
if (!this.listeners[event]) {
|
|
22
|
+
this.listeners[event] = [];
|
|
23
|
+
}
|
|
24
|
+
this.listeners[event].push(callback);
|
|
25
|
+
|
|
26
|
+
// Return unsubscribe function
|
|
27
|
+
return () => {
|
|
28
|
+
this.listeners[event] = this.listeners[event].filter(l => l !== callback);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
emit<T>(event: string, data: T): void {
|
|
33
|
+
if (!this.listeners[event]) return;
|
|
34
|
+
this.listeners[event].forEach(callback => {
|
|
35
|
+
try {
|
|
36
|
+
callback(data);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (__DEV__) console.error(`[SubscriptionEventBus] Error in listener for ${event}:`, error);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const subscriptionEventBus = SubscriptionEventBus.getInstance();
|
|
45
|
+
|
|
46
|
+
export const SUBSCRIPTION_EVENTS = {
|
|
47
|
+
CREDITS_UPDATED: "credits_updated",
|
|
48
|
+
PURCHASE_COMPLETED: "purchase_completed",
|
|
49
|
+
RENEWAL_DETECTED: "renewal_detected",
|
|
50
|
+
PREMIUM_STATUS_CHANGED: "premium_status_changed",
|
|
51
|
+
};
|
|
@@ -7,23 +7,22 @@ import { PACKAGE_TYPE, type PackageType } from "../domain/entities/SubscriptionC
|
|
|
7
7
|
|
|
8
8
|
export type SubscriptionPackageType = PackageType;
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Check if identifier is a credit package (consumable purchase)
|
|
12
|
+
* Credit packages use a different system and don't need type detection
|
|
13
|
+
*/
|
|
10
14
|
/**
|
|
11
15
|
* Check if identifier is a credit package (consumable purchase)
|
|
12
16
|
* Credit packages use a different system and don't need type detection
|
|
13
17
|
*/
|
|
14
18
|
function isCreditPackage(identifier: string): boolean {
|
|
15
|
-
|
|
19
|
+
// Matches "credit" as a word or part of a common naming pattern
|
|
20
|
+
return /\bcredit\b|_credit_|-credit-/i.test(identifier) || identifier.toLowerCase().includes("credit");
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
/**
|
|
19
24
|
* Detect package type from product identifier
|
|
20
|
-
* Supports common RevenueCat naming patterns
|
|
21
|
-
* - premium_weekly, weekly_premium, premium-weekly
|
|
22
|
-
* - premium_monthly, monthly_premium, premium-monthly
|
|
23
|
-
* - premium_yearly, yearly_premium, premium-yearly, premium_annual, annual_premium
|
|
24
|
-
* - preview-product-id (Preview API mode in Expo Go)
|
|
25
|
-
*
|
|
26
|
-
* Note: Credit packages (consumable purchases) are skipped silently
|
|
25
|
+
* Supports common RevenueCat naming patterns with regex for better accuracy
|
|
27
26
|
*/
|
|
28
27
|
export function detectPackageType(productIdentifier: string): SubscriptionPackageType {
|
|
29
28
|
if (!productIdentifier) {
|
|
@@ -45,28 +44,24 @@ export function detectPackageType(productIdentifier: string): SubscriptionPackag
|
|
|
45
44
|
return PACKAGE_TYPE.MONTHLY;
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
// Weekly detection
|
|
49
|
-
if (
|
|
47
|
+
// Weekly detection: matches "weekly" or "week" as distinct parts of the ID
|
|
48
|
+
if (/\bweekly?\b|_week_|-week-|\.week\./i.test(normalized)) {
|
|
50
49
|
if (__DEV__) {
|
|
51
50
|
console.log("[PackageTypeDetector] Detected: WEEKLY");
|
|
52
51
|
}
|
|
53
52
|
return PACKAGE_TYPE.WEEKLY;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
// Monthly detection
|
|
57
|
-
if (
|
|
55
|
+
// Monthly detection: matches "monthly" or "month"
|
|
56
|
+
if (/\bmonthly?\b|_month_|-month-|\.month\./i.test(normalized)) {
|
|
58
57
|
if (__DEV__) {
|
|
59
58
|
console.log("[PackageTypeDetector] Detected: MONTHLY");
|
|
60
59
|
}
|
|
61
60
|
return PACKAGE_TYPE.MONTHLY;
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
// Yearly detection
|
|
65
|
-
if (
|
|
66
|
-
normalized.includes("yearly") ||
|
|
67
|
-
normalized.includes("year") ||
|
|
68
|
-
normalized.includes("annual")
|
|
69
|
-
) {
|
|
63
|
+
// Yearly detection: matches "yearly", "year", or "annual"
|
|
64
|
+
if (/\byearly?\b|_year_|-year-|\.year\.|annual/i.test(normalized)) {
|
|
70
65
|
if (__DEV__) {
|
|
71
66
|
console.log("[PackageTypeDetector] Detected: YEARLY");
|
|
72
67
|
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# Application Layer
|
|
2
|
-
|
|
3
|
-
Abonelik uygulamasının iş mantığını ve servis kontratlarını içeren katman.
|
|
4
|
-
|
|
5
|
-
## Location
|
|
6
|
-
|
|
7
|
-
`src/application/`
|
|
8
|
-
|
|
9
|
-
## Strategy
|
|
10
|
-
|
|
11
|
-
Ortak kullanım durumlarını gerçekleştiren, servis kontratlarını tanımlayan ve uygulama kurallarını yöneten katman. Dependency injection ve test edilebilirlik sağlar.
|
|
12
|
-
|
|
13
|
-
## Restrictions
|
|
14
|
-
|
|
15
|
-
### REQUIRED
|
|
16
|
-
|
|
17
|
-
- MUST use dependency injection pattern
|
|
18
|
-
- MUST define service contracts through interfaces
|
|
19
|
-
- MUST implement error handling and propagation
|
|
20
|
-
- MUST ensure type safety for all operations
|
|
21
|
-
- MUST support dependency inversion principle
|
|
22
|
-
|
|
23
|
-
### PROHIBITED
|
|
24
|
-
|
|
25
|
-
- MUST NOT contain direct infrastructure dependencies
|
|
26
|
-
- MUST NOT bypass repository interfaces
|
|
27
|
-
- MUST NOT leak implementation details
|
|
28
|
-
- MUST NOT mix business logic with presentation
|
|
29
|
-
|
|
30
|
-
### CRITICAL
|
|
31
|
-
|
|
32
|
-
- Keep interfaces small and focused (Interface Segregation)
|
|
33
|
-
- Maintain single responsibility for each service
|
|
34
|
-
- Always handle errors appropriately and propagate upward
|
|
35
|
-
- Ensure all operations are type-safe
|
|
36
|
-
|
|
37
|
-
## AI Agent Guidelines
|
|
38
|
-
|
|
39
|
-
When working with application layer:
|
|
40
|
-
1. Interface Segregation - küçük, odaklanmış interface'ler tanımlayın
|
|
41
|
-
2. Dependency Inversion - high-level modüller low-level modüllere bağımlı olmamalı
|
|
42
|
-
3. Single Responsibility - her service/repository tek bir sorumluluğa sahip olmalı
|
|
43
|
-
4. Error Handling - hataları uygun şekilde handle edin ve yukarı propagation edin
|
|
44
|
-
5. Testing - interface'ler mock ile kolay test edilebilir
|
|
45
|
-
|
|
46
|
-
## Related Documentation
|
|
47
|
-
|
|
48
|
-
- [Domain Layer](../domain/README.md)
|
|
49
|
-
- [Infrastructure Layer](../infrastructure/README.md)
|
|
50
|
-
- [Ports](./ports/README.md)
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# Domain Entities
|
|
2
|
-
|
|
3
|
-
Core domain entities for subscription management.
|
|
4
|
-
|
|
5
|
-
## Location
|
|
6
|
-
|
|
7
|
-
`src/domain/entities/`
|
|
8
|
-
|
|
9
|
-
## Strategy
|
|
10
|
-
|
|
11
|
-
Domain entities represent the core business concepts and rules of the subscription system. They are framework-agnostic and contain only business logic, ensuring pure domain modeling.
|
|
12
|
-
|
|
13
|
-
## Restrictions
|
|
14
|
-
|
|
15
|
-
### REQUIRED
|
|
16
|
-
|
|
17
|
-
- MUST validate themselves on creation
|
|
18
|
-
- MUST remain immutable after creation
|
|
19
|
-
- MUST encapsulate business logic internally
|
|
20
|
-
- MUST use value objects for complex attributes
|
|
21
|
-
- MUST be framework-agnostic
|
|
22
|
-
|
|
23
|
-
### PROHIBITED
|
|
24
|
-
|
|
25
|
-
- MUST NOT have framework dependencies
|
|
26
|
-
- MUST NOT expose internal state directly
|
|
27
|
-
- MUST NOT allow direct state modification
|
|
28
|
-
- MUST NOT contain infrastructure concerns
|
|
29
|
-
|
|
30
|
-
### CRITICAL
|
|
31
|
-
|
|
32
|
-
- Always validate invariants to ensure valid state
|
|
33
|
-
- Prevent direct state modification through immutability
|
|
34
|
-
- Keep business rules encapsulated within entities
|
|
35
|
-
- Maintain purity - no external dependencies
|
|
36
|
-
|
|
37
|
-
## AI Agent Guidelines
|
|
38
|
-
|
|
39
|
-
When working with domain entities:
|
|
40
|
-
1. Keep entities pure - no framework dependencies
|
|
41
|
-
2. Validate invariants - ensure valid state
|
|
42
|
-
3. Use value objects - for complex attributes
|
|
43
|
-
4. Encapsulate logic - keep business rules inside entities
|
|
44
|
-
5. Make immutable - prevent direct state modification
|
|
45
|
-
|
|
46
|
-
## Related Documentation
|
|
47
|
-
|
|
48
|
-
- [Value Objects](../value-objects/README.md)
|
|
49
|
-
- [Domain Errors](../errors/README.md)
|
|
50
|
-
- [Domain Layer](../README.md)
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createDefaultSubscriptionStatus,
|
|
3
|
-
isSubscriptionValid,
|
|
4
|
-
calculateDaysRemaining,
|
|
5
|
-
} from './SubscriptionStatus';
|
|
6
|
-
|
|
7
|
-
describe('SubscriptionStatus', () => {
|
|
8
|
-
describe('createDefaultSubscriptionStatus', () => {
|
|
9
|
-
it('should create default subscription status', () => {
|
|
10
|
-
const status = createDefaultSubscriptionStatus();
|
|
11
|
-
|
|
12
|
-
expect(status).toEqual({
|
|
13
|
-
isPremium: false,
|
|
14
|
-
expiresAt: null,
|
|
15
|
-
productId: null,
|
|
16
|
-
purchasedAt: null,
|
|
17
|
-
customerId: null,
|
|
18
|
-
syncedAt: null,
|
|
19
|
-
status: 'none',
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('isSubscriptionValid', () => {
|
|
25
|
-
it('should return false for null status', () => {
|
|
26
|
-
expect(isSubscriptionValid(null)).toBe(false);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should return false for non-premium status', () => {
|
|
30
|
-
const status = {
|
|
31
|
-
isPremium: false,
|
|
32
|
-
expiresAt: null,
|
|
33
|
-
productId: null,
|
|
34
|
-
purchasedAt: null,
|
|
35
|
-
customerId: null,
|
|
36
|
-
syncedAt: null,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
expect(isSubscriptionValid(status)).toBe(false);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should return true for lifetime subscription', () => {
|
|
43
|
-
const status = {
|
|
44
|
-
isPremium: true,
|
|
45
|
-
expiresAt: null,
|
|
46
|
-
productId: 'lifetime',
|
|
47
|
-
purchasedAt: '2024-01-01T00:00:00.000Z',
|
|
48
|
-
customerId: 'customer123',
|
|
49
|
-
syncedAt: '2024-01-01T00:00:00.000Z',
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
expect(isSubscriptionValid(status)).toBe(true);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should return true for active subscription', () => {
|
|
56
|
-
const futureDate = new Date();
|
|
57
|
-
futureDate.setDate(futureDate.getDate() + 30);
|
|
58
|
-
|
|
59
|
-
const status = {
|
|
60
|
-
isPremium: true,
|
|
61
|
-
expiresAt: futureDate.toISOString(),
|
|
62
|
-
productId: 'monthly',
|
|
63
|
-
purchasedAt: '2024-01-01T00:00:00.000Z',
|
|
64
|
-
customerId: 'customer123',
|
|
65
|
-
syncedAt: '2024-01-01T00:00:00.000Z',
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
expect(isSubscriptionValid(status)).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('should return false for expired subscription', () => {
|
|
72
|
-
const pastDate = new Date();
|
|
73
|
-
pastDate.setDate(pastDate.getDate() - 1);
|
|
74
|
-
|
|
75
|
-
const status = {
|
|
76
|
-
isPremium: true,
|
|
77
|
-
expiresAt: pastDate.toISOString(),
|
|
78
|
-
productId: 'monthly',
|
|
79
|
-
purchasedAt: '2024-01-01T00:00:00.000Z',
|
|
80
|
-
customerId: 'customer123',
|
|
81
|
-
syncedAt: '2024-01-01T00:00:00.000Z',
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
expect(isSubscriptionValid(status)).toBe(false);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe('calculateDaysRemaining', () => {
|
|
89
|
-
it('should return null for null input', () => {
|
|
90
|
-
expect(calculateDaysRemaining(null)).toBeNull();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should return positive days for future expiration', () => {
|
|
94
|
-
const futureDate = new Date();
|
|
95
|
-
futureDate.setDate(futureDate.getDate() + 5);
|
|
96
|
-
expect(calculateDaysRemaining(futureDate.toISOString())).toBe(5);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should return 0 for past expiration', () => {
|
|
100
|
-
const pastDate = new Date();
|
|
101
|
-
pastDate.setDate(pastDate.getDate() - 5);
|
|
102
|
-
expect(calculateDaysRemaining(pastDate.toISOString())).toBe(0);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
});
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# Domain Errors
|
|
2
|
-
|
|
3
|
-
Domain-specific error types for subscription system.
|
|
4
|
-
|
|
5
|
-
## Location
|
|
6
|
-
|
|
7
|
-
`src/domain/errors/`
|
|
8
|
-
|
|
9
|
-
## Strategy
|
|
10
|
-
|
|
11
|
-
Domain errors provide typed, contextual error handling for business logic failures. They make error handling explicit and type-safe, enabling precise error management.
|
|
12
|
-
|
|
13
|
-
## Restrictions
|
|
14
|
-
|
|
15
|
-
### REQUIRED
|
|
16
|
-
|
|
17
|
-
- MUST use specific error types (not generic Error)
|
|
18
|
-
- MUST include contextual information
|
|
19
|
-
- MUST document all error codes
|
|
20
|
-
- MUST handle errors gracefully with user-friendly messages
|
|
21
|
-
- MUST log errors for debugging
|
|
22
|
-
- MUST use type guards for type-safe error handling
|
|
23
|
-
|
|
24
|
-
### PROHIBITED
|
|
25
|
-
|
|
26
|
-
- MUST NOT swallow errors without handling
|
|
27
|
-
- MUST NOT use generic Error class
|
|
28
|
-
- MUST NOT expose sensitive information in error messages
|
|
29
|
-
- MUST NOT ignore error conditions
|
|
30
|
-
|
|
31
|
-
### CRITICAL
|
|
32
|
-
|
|
33
|
-
- Always handle or rethrow errors
|
|
34
|
-
- Include relevant context in error objects
|
|
35
|
-
- Use type guards to enable type-safe error handling
|
|
36
|
-
- Show user-friendly messages while logging technical details
|
|
37
|
-
|
|
38
|
-
## AI Agent Guidelines
|
|
39
|
-
|
|
40
|
-
When working with domain errors:
|
|
41
|
-
1. Use specific error types - don't use generic Error
|
|
42
|
-
2. Include context - add relevant data to errors
|
|
43
|
-
3. Document error codes - list all possible errors
|
|
44
|
-
4. Handle gracefully - show user-friendly messages
|
|
45
|
-
5. Log errors - track for debugging
|
|
46
|
-
6. Don't swallow errors - always handle or rethrow
|
|
47
|
-
7. Use type guards - enable type-safe error handling
|
|
48
|
-
|
|
49
|
-
## Related Documentation
|
|
50
|
-
|
|
51
|
-
- [Domain Entities](../entities/README.md)
|
|
52
|
-
- [Value Objects](../value-objects/README.md)
|
|
53
|
-
- [Domain Layer](../README.md)
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# Domain Value Objects
|
|
2
|
-
|
|
3
|
-
Value objects for the subscription domain.
|
|
4
|
-
|
|
5
|
-
## Location
|
|
6
|
-
|
|
7
|
-
`src/domain/value-objects/`
|
|
8
|
-
|
|
9
|
-
## Strategy
|
|
10
|
-
|
|
11
|
-
Value objects are immutable objects that represent concepts by their attributes rather than identity. They ensure validity and prevent primitive obsession by providing type-safe, validated representations.
|
|
12
|
-
|
|
13
|
-
## Restrictions
|
|
14
|
-
|
|
15
|
-
### REQUIRED
|
|
16
|
-
|
|
17
|
-
- MUST be immutable (all properties readonly)
|
|
18
|
-
- MUST validate on creation (fail fast)
|
|
19
|
-
- MUST override equality (compare by value, not reference)
|
|
20
|
-
- MUST use for complex attributes (not simple primitives)
|
|
21
|
-
- MUST be small and focused
|
|
22
|
-
|
|
23
|
-
### PROHIBITED
|
|
24
|
-
|
|
25
|
-
- MUST NOT allow mutation after creation
|
|
26
|
-
- MUST NOT use reference equality for comparison
|
|
27
|
-
- MUST NOT contain invalid states
|
|
28
|
-
- MUST NOT have identity-based equality
|
|
29
|
-
|
|
30
|
-
### CRITICAL
|
|
31
|
-
|
|
32
|
-
- Always validate on creation
|
|
33
|
-
- Implement value-based equality comparison
|
|
34
|
-
- Keep value objects small and focused
|
|
35
|
-
- Use only for complex attributes, not simple primitives
|
|
36
|
-
|
|
37
|
-
## AI Agent Guidelines
|
|
38
|
-
|
|
39
|
-
When working with value objects:
|
|
40
|
-
1. Make immutable - all properties readonly
|
|
41
|
-
2. Validate on creation - fail fast
|
|
42
|
-
3. Override equality - compare by value, not reference
|
|
43
|
-
4. Use for complex attributes - don't use for simple primitives
|
|
44
|
-
5. Keep small - value objects should be focused
|
|
45
|
-
|
|
46
|
-
## Related Documentation
|
|
47
|
-
|
|
48
|
-
- [Domain Entities](../entities/README.md)
|
|
49
|
-
- [Domain Errors](../errors/README.md)
|
|
50
|
-
- [Domain Layer](../README.md)
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# Infrastructure Layer
|
|
2
|
-
|
|
3
|
-
Abonelik sisteminin dış dünya ile iletişimini sağlayan implementations ve repositories içeren katman.
|
|
4
|
-
|
|
5
|
-
## Location
|
|
6
|
-
|
|
7
|
-
`src/infrastructure/`
|
|
8
|
-
|
|
9
|
-
## Strategy
|
|
10
|
-
|
|
11
|
-
Dış servis entegrasyonlarını (Firebase, RevenueCat), veri erişim implementasyonlarını ve karmaşık operasyon yöneticilerini içeren katman. Dependency injection ve test edilebilirlik sağlar.
|
|
12
|
-
|
|
13
|
-
## Restrictions
|
|
14
|
-
|
|
15
|
-
### REQUIRED
|
|
16
|
-
|
|
17
|
-
- MUST implement all port interfaces from application layer
|
|
18
|
-
- MUST handle all errors gracefully and propagate appropriately
|
|
19
|
-
- MUST validate all inputs before processing
|
|
20
|
-
- MUST implement caching for frequently accessed data
|
|
21
|
-
- MUST support logging for debugging and monitoring
|
|
22
|
-
- MUST be testable with mock implementations
|
|
23
|
-
|
|
24
|
-
### PROHIBITED
|
|
25
|
-
|
|
26
|
-
- MUST NOT contain business logic (belongs in domain/application)
|
|
27
|
-
- MUST NOT bypass error handling
|
|
28
|
-
- MUST NOT expose implementation details to other layers
|
|
29
|
-
- MUST NOT create tight coupling with external services
|
|
30
|
-
|
|
31
|
-
### CRITICAL
|
|
32
|
-
|
|
33
|
-
- Always validate inputs before processing
|
|
34
|
-
- Implement proper error handling for all external calls
|
|
35
|
-
- Use dependency injection for all external dependencies
|
|
36
|
-
- Ensure all implementations are mockable for testing
|
|
37
|
-
- Implement retry logic for network operations
|
|
38
|
-
|
|
39
|
-
## AI Agent Guidelines
|
|
40
|
-
|
|
41
|
-
When working with infrastructure layer:
|
|
42
|
-
1. Dependency Injection - repository'leri constructor'da alın
|
|
43
|
-
2. Error Handling - tüm hataları yakalayın ve uygun şekilde handle edin
|
|
44
|
-
3. Caching - sık kullanılan verileri cache'leyin
|
|
45
|
-
4. Validation - girdileri validate edin
|
|
46
|
-
5. Logging - önemli operasyonları log'layın
|
|
47
|
-
6. Testing - mock implementasyonlarla test edilebilir yapın
|
|
48
|
-
7. Retry Logic - network hataları için retry logic ekleyin
|
|
49
|
-
|
|
50
|
-
## Related Documentation
|
|
51
|
-
|
|
52
|
-
- [Application Layer](../application/README.md)
|
|
53
|
-
- [Domain Layer](../domain/README.md)
|
|
54
|
-
- [Repositories](./repositories/README.md)
|
|
55
|
-
- [Services](./services/README.md)
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# Infrastructure Mappers
|
|
2
|
-
|
|
3
|
-
Data transformation mappers between layers.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
This directory contains mapper functions that transform data between different layers (e.g., domain entities to DTOs, external API responses to domain models).
|
|
8
|
-
|
|
9
|
-
## Contents
|
|
10
|
-
|
|
11
|
-
- **subscriptionMapper.ts** - Maps between subscription entities and external formats
|
|
12
|
-
- **creditsMapper.ts** - Maps between credit entities and Firestore documents
|
|
13
|
-
|
|
14
|
-
## Purpose
|
|
15
|
-
|
|
16
|
-
Mappers provide clean separation between layers:
|
|
17
|
-
|
|
18
|
-
## Related
|
|
19
|
-
|
|
20
|
-
- [Models](../models/README.md)
|
|
21
|
-
- [Domain](../../domain/README.md)
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# Infrastructure Models
|
|
2
|
-
|
|
3
|
-
Data models and schemas used by the infrastructure layer.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
This directory contains data models, schemas, and interfaces used by infrastructure implementations.
|
|
8
|
-
|
|
9
|
-
## Contents
|
|
10
|
-
|
|
11
|
-
- **SubscriptionModel.ts** - Subscription data model for persistence/transport
|
|
12
|
-
- **CreditsModel.ts** - Credits data model for Firestore
|
|
13
|
-
|
|
14
|
-
## Purpose
|
|
15
|
-
|
|
16
|
-
Models provide structure for data storage and API communication:
|
|
17
|
-
|
|
18
|
-
- **Validation**: Ensure data integrity
|
|
19
|
-
- **Serialization**: Convert to/from storage formats
|
|
20
|
-
- **Type Safety**: Provide TypeScript interfaces
|
|
21
|
-
|
|
22
|
-
## Related
|
|
23
|
-
|
|
24
|
-
- [Repositories](../repositories/README.md)
|
|
25
|
-
- [Services](../services/README.md)
|
|
26
|
-
- [Domain Entities](../../domain/entities/README.md)
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credits Repository
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { doc, getDoc, runTransaction, serverTimestamp, type Firestore, type Transaction } from "firebase/firestore";
|
|
6
|
-
import { BaseRepository, getFirestore } from "@umituz/react-native-firebase";
|
|
7
|
-
import type { CreditsConfig, CreditsResult, DeductCreditsResult } from "../../domain/entities/Credits";
|
|
8
|
-
import type { UserCreditsDocumentRead, PurchaseSource } from "../models/UserCreditsDocument";
|
|
9
|
-
import { initializeCreditsTransaction, type InitializeCreditsMetadata } from "../services/CreditsInitializer";
|
|
10
|
-
import { detectPackageType } from "../../utils/packageTypeDetector";
|
|
11
|
-
import { getCreditAllocation } from "../../utils/creditMapper";
|
|
12
|
-
import { CreditsMapper } from "../mappers/CreditsMapper";
|
|
13
|
-
import type { RevenueCatData } from "../../domain/types/RevenueCatData";
|
|
14
|
-
|
|
15
|
-
export type { RevenueCatData } from "../../domain/types/RevenueCatData";
|
|
16
|
-
|
|
17
|
-
export class CreditsRepository extends BaseRepository {
|
|
18
|
-
constructor(private config: CreditsConfig) { super(); }
|
|
19
|
-
|
|
20
|
-
private getRef(db: Firestore, userId: string) {
|
|
21
|
-
return this.config.useUserSubcollection
|
|
22
|
-
? doc(db, "users", userId, "credits", "balance")
|
|
23
|
-
: doc(db, this.config.collectionName, userId);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async getCredits(userId: string): Promise<CreditsResult> {
|
|
27
|
-
const db = getFirestore();
|
|
28
|
-
if (!db) {
|
|
29
|
-
if (__DEV__) console.log("[CreditsRepository] No Firestore instance");
|
|
30
|
-
return { success: false, error: { message: "No DB", code: "DB_ERR" } };
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
const ref = this.getRef(db, userId);
|
|
34
|
-
if (__DEV__) console.log("[CreditsRepository] Fetching credits:", { userId: userId.slice(0, 8), path: ref.path });
|
|
35
|
-
const snap = await getDoc(ref);
|
|
36
|
-
if (!snap.exists()) {
|
|
37
|
-
if (__DEV__) console.log("[CreditsRepository] No credits document found");
|
|
38
|
-
return { success: true, data: undefined };
|
|
39
|
-
}
|
|
40
|
-
const d = snap.data() as UserCreditsDocumentRead;
|
|
41
|
-
const entity = CreditsMapper.toEntity(d);
|
|
42
|
-
if (__DEV__) console.log("[CreditsRepository] Credits fetched:", { credits: entity.credits, limit: entity.creditLimit });
|
|
43
|
-
return { success: true, data: entity };
|
|
44
|
-
} catch (e: unknown) {
|
|
45
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
46
|
-
if (__DEV__) console.error("[CreditsRepository] Fetch error:", message);
|
|
47
|
-
return { success: false, error: { message, code: "FETCH_ERR" } };
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async initializeCredits(
|
|
52
|
-
userId: string, purchaseId?: string, productId?: string,
|
|
53
|
-
source?: PurchaseSource, revenueCatData?: RevenueCatData
|
|
54
|
-
): Promise<CreditsResult> {
|
|
55
|
-
const db = getFirestore();
|
|
56
|
-
if (!db) return { success: false, error: { message: "No DB", code: "INIT_ERR" } };
|
|
57
|
-
try {
|
|
58
|
-
let cfg = { ...this.config };
|
|
59
|
-
if (productId) {
|
|
60
|
-
const amt = this.config.creditPackageAmounts?.[productId];
|
|
61
|
-
if (amt) cfg = { ...cfg, creditLimit: amt };
|
|
62
|
-
else {
|
|
63
|
-
const packageType = detectPackageType(productId);
|
|
64
|
-
const dynamicLimit = getCreditAllocation(packageType, this.config.packageAllocations);
|
|
65
|
-
if (dynamicLimit !== null) cfg = { ...cfg, creditLimit: dynamicLimit };
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const metadata: InitializeCreditsMetadata = {
|
|
70
|
-
productId, source,
|
|
71
|
-
expirationDate: revenueCatData?.expirationDate,
|
|
72
|
-
willRenew: revenueCatData?.willRenew,
|
|
73
|
-
originalTransactionId: revenueCatData?.originalTransactionId,
|
|
74
|
-
isPremium: revenueCatData?.isPremium,
|
|
75
|
-
periodType: revenueCatData?.periodType,
|
|
76
|
-
};
|
|
77
|
-
|
|
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;
|
|
82
|
-
return {
|
|
83
|
-
success: true,
|
|
84
|
-
data: fullData ? CreditsMapper.toEntity(fullData) : undefined,
|
|
85
|
-
};
|
|
86
|
-
} catch (e: unknown) {
|
|
87
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
88
|
-
return { success: false, error: { message, code: "INIT_ERR" } };
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async deductCredit(userId: string, cost: number = 1): Promise<DeductCreditsResult> {
|
|
93
|
-
const db = getFirestore();
|
|
94
|
-
if (!db) return { success: false, error: { message: "No DB", code: "ERR" } };
|
|
95
|
-
try {
|
|
96
|
-
const remaining = await runTransaction(db, async (tx: Transaction) => {
|
|
97
|
-
const docSnap = await tx.get(this.getRef(db, userId));
|
|
98
|
-
if (!docSnap.exists()) throw new Error("NO_CREDITS");
|
|
99
|
-
const current = docSnap.data().credits as number;
|
|
100
|
-
if (current < cost) throw new Error("CREDITS_EXHAUSTED");
|
|
101
|
-
const updated = current - cost;
|
|
102
|
-
tx.update(this.getRef(db, userId), { credits: updated, lastUpdatedAt: serverTimestamp() });
|
|
103
|
-
return updated;
|
|
104
|
-
});
|
|
105
|
-
return { success: true, remainingCredits: remaining };
|
|
106
|
-
} catch (e: unknown) {
|
|
107
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
108
|
-
const code = message === "NO_CREDITS" || message === "CREDITS_EXHAUSTED" ? message : "DEDUCT_ERR";
|
|
109
|
-
return { success: false, error: { message, code } };
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async hasCredits(userId: string, cost: number = 1): Promise<boolean> {
|
|
114
|
-
const res = await this.getCredits(userId);
|
|
115
|
-
return !!(res.success && res.data && res.data.credits >= cost);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async syncExpiredStatus(userId: string): Promise<void> {
|
|
119
|
-
const db = getFirestore();
|
|
120
|
-
if (!db) return;
|
|
121
|
-
try {
|
|
122
|
-
const ref = this.getRef(db, userId);
|
|
123
|
-
const { updateDoc } = await import("firebase/firestore");
|
|
124
|
-
await updateDoc(ref, { isPremium: false, status: "expired", lastUpdatedAt: serverTimestamp() });
|
|
125
|
-
if (__DEV__) console.log("[CreditsRepository] Synced expired status:", userId.slice(0, 8));
|
|
126
|
-
} catch (e) {
|
|
127
|
-
if (__DEV__) console.error("[CreditsRepository] Sync expired failed:", e);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export const createCreditsRepository = (c: CreditsConfig) => new CreditsRepository(c);
|