diff --git a/backend/services/billing/__tests__/billingEngine.test.ts b/backend/services/billing/__tests__/billingEngine.test.ts new file mode 100644 index 00000000..58c7c4c8 --- /dev/null +++ b/backend/services/billing/__tests__/billingEngine.test.ts @@ -0,0 +1,82 @@ +import { BillingEngine } from '../billingEngine'; +import { PricingStrategyFactory } from '../strategyFactory'; +import { PricingContext } from '../pricingStrategy'; + +describe('BillingEngine Strategy Pattern Integration', () => { + beforeEach(() => { + PricingStrategyFactory.reset(); + }); + + const sampleContext: PricingContext = { + planId: 'plan-pro-01', + subscriberAddress: '0x1234567890abcdef', + currentPrice: 49.99, + currency: 'USD', + usageData: { + sessionsPerWeek: 12, + retentionRate: 0.95, + apiCallsThisPeriod: 5500, + storageUsedMB: 1024, + seatsActive: 5, + }, + }; + + it('should initialize with default config', () => { + const engine = new BillingEngine(); + expect(engine.getAvailableStrategies()).toEqual([ + 'flat_rate', + 'usage_based', + 'tiered', + 'dynamic', + ]); + }); + + it('should calculate price using default flat_rate for basic plan', () => { + const engine = new BillingEngine(); + const result = engine.calculatePrice('basic', sampleContext); + + expect(result.strategyName).toBe('flat_rate'); + expect(result.price).toBe(49.99); + expect(result.breakdown.finalPrice).toBe(49.99); + }); + + it('should calculate price using tiered strategy for premium plan', () => { + const engine = new BillingEngine(); + const result = engine.calculatePrice('premium', sampleContext); + + expect(result.strategyName).toBe('tiered'); + expect(result.price).toBeGreaterThan(0); + }); + + it('should allow runtime strategy override', () => { + const engine = new BillingEngine(); + const result = engine.calculatePrice('basic', sampleContext, 'usage_based'); + + expect(result.strategyName).toBe('usage_based'); + }); + + it('should process billing charge and record history', () => { + const engine = new BillingEngine(); + const record = engine.processCharge('enterprise', sampleContext); + + expect(record.subscriptionId).toBe('plan-pro-01'); + expect(record.amount).toBeGreaterThan(0); + expect(record.currency).toBe('USD'); + + const history = engine.getBillingHistory('0x1234567890abcdef'); + expect(history.length).toBe(1); + expect(history[0].subscriptionId).toBe('plan-pro-01'); + }); + + it('should generate A/B test variants and analytics', () => { + const engine = new BillingEngine(); + engine.calculatePrice('basic', sampleContext); + + const variants = engine.getABTestVariants('basic', 49.99); + expect(variants.length).toBeGreaterThan(0); + + const analytics = engine.getAnalytics('flat_rate'); + expect(analytics.length).toBe(1); + expect(analytics[0].totalCalculations).toBe(2); // calculatePrice + getABTestVariants + }); +}); diff --git a/backend/services/notification/alerting.ts b/backend/services/notification/alerting.ts index 29dac686..2e1ccd6e 100644 --- a/backend/services/notification/alerting.ts +++ b/backend/services/notification/alerting.ts @@ -3,12 +3,8 @@ * Channels are pluggable; add as many as needed. */ -<<<<<<< HEAD:backend/services/alerting.ts -import { logger } from './logging'; -import type { Alert, AlertChannelConfig } from './types'; -======= +import { logger } from '../shared/logging'; import type { Alert, AlertChannelConfig } from '../shared/types'; ->>>>>>> main:backend/services/notification/alerting.ts export interface AlertDispatcher { send(alert: Alert): Promise; diff --git a/backend/services/shared/__tests__/authStrategies.test.ts b/backend/services/shared/__tests__/authStrategies.test.ts new file mode 100644 index 00000000..88502c62 --- /dev/null +++ b/backend/services/shared/__tests__/authStrategies.test.ts @@ -0,0 +1,105 @@ +import { Request } from 'express'; +import { + JwtAuthStrategy, + ApiKeyAuthStrategy, + WalletAuthStrategy, + OAuthSessionAuthStrategy, + CompositeAuthStrategyManager, + createRequireRoleMiddleware, + createRequireStrategyMiddleware, +} from '../authStrategies'; +import { UnauthorizedError, ForbiddenError } from '../errors'; + +describe('Auth Strategies', () => { + it('validates JWT token strategy', async () => { + const strategy = new JwtAuthStrategy(); + const mockReq = { headers: { authorization: 'Bearer valid.jwt.token' } } as Request; + const user = await strategy.validate(mockReq); + + expect(user).not.toBeNull(); + expect(user?.strategy).toBe('jwt'); + expect(user?.roles).toContain('user'); + }); + + it('rejects invalid JWT token', async () => { + const strategy = new JwtAuthStrategy(); + const mockReq = { headers: { authorization: 'Bearer invalid-token' } } as Request; + const user = await strategy.validate(mockReq); + + expect(user).toBeNull(); + }); + + it('validates API key strategy', async () => { + const strategy = new ApiKeyAuthStrategy(); + const mockReq = { headers: { 'x-api-key': 'valid-api-key-12345' } } as Request; + const user = await strategy.validate(mockReq); + + expect(user).not.toBeNull(); + expect(user?.strategy).toBe('api-key'); + expect(user?.roles).toContain('api_client'); + }); + + it('validates OAuth session strategy', async () => { + const strategy = new OAuthSessionAuthStrategy(); + const mockReq = { headers: { 'x-session-id': 'sess_abc123' } } as Request; + const user = await strategy.validate(mockReq); + + expect(user).not.toBeNull(); + expect(user?.strategy).toBe('oauth-session'); + expect(user?.roles).toContain('oauth_user'); + }); + + it('runs composite authentication manager across multiple strategies', async () => { + const manager = new CompositeAuthStrategyManager([ + new JwtAuthStrategy(), + new ApiKeyAuthStrategy(), + new WalletAuthStrategy(), + new OAuthSessionAuthStrategy(), + ]); + + const reqWithApiKey = { headers: { 'x-api-key': 'valid-api-key-123' }, query: {} } as Request; + const user = await manager.authenticate(reqWithApiKey); + + expect(user.strategy).toBe('api-key'); + }); + + it('throws UnauthorizedError when all strategies fail', async () => { + const manager = new CompositeAuthStrategyManager([ + new JwtAuthStrategy(), + new ApiKeyAuthStrategy(), + ]); + + const emptyReq = { headers: {}, query: {} } as Request; + await expect(manager.authenticate(emptyReq)).rejects.toThrow(UnauthorizedError); + }); + + it('enforces role authorization middleware', () => { + const middleware = createRequireRoleMiddleware(['admin', 'api_client']); + + const reqWithRole = { user: { id: '123', roles: ['api_client'], strategy: 'api-key' } } as any; + const reqWithoutRole = { user: { id: '456', roles: ['user'], strategy: 'jwt' } } as any; + const next = jest.fn(); + + middleware(reqWithRole, {} as any, next); + expect(next).toHaveBeenCalledWith(); + + next.mockClear(); + middleware(reqWithoutRole, {} as any, next); + expect(next).toHaveBeenCalledWith(expect.any(ForbiddenError)); + }); + + it('enforces strategy requirement middleware', () => { + const middleware = createRequireStrategyMiddleware(['api-key', 'jwt']); + + const validReq = { user: { id: '123', roles: [], strategy: 'api-key' } } as any; + const invalidReq = { user: { id: '456', roles: [], strategy: 'wallet' } } as any; + const next = jest.fn(); + + middleware(validReq, {} as any, next); + expect(next).toHaveBeenCalledWith(); + + next.mockClear(); + middleware(invalidReq, {} as any, next); + expect(next).toHaveBeenCalledWith(expect.any(ForbiddenError)); + }); +}); diff --git a/backend/services/shared/__tests__/errors.test.ts b/backend/services/shared/__tests__/errors.test.ts new file mode 100644 index 00000000..2418f1ae --- /dev/null +++ b/backend/services/shared/__tests__/errors.test.ts @@ -0,0 +1,62 @@ +import { + DomainError, + UnprocessableEntityError, + BadGatewayError, + isDomainError, + fromUnknownError, +} from '../errors'; + +describe('Structured Error Types', () => { + it('creates DomainError with structured fields', () => { + const err = new DomainError('Test error', 'TEST_CODE', 400, { + userMessage: 'Friendly user message', + recovery: 'Try again later', + details: { foo: 'bar' }, + requestId: 'req_123', + }); + + expect(err.message).toBe('Test error'); + expect(err.code).toBe('TEST_CODE'); + expect(err.statusCode).toBe(400); + expect(err.userMessage).toBe('Friendly user message'); + expect(err.recovery).toBe('Try again later'); + expect(err.details).toEqual({ foo: 'bar' }); + expect(err.requestId).toBe('req_123'); + + const apiRes = err.toApiResponse(); + expect(apiRes.error.code).toBe('TEST_CODE'); + expect(apiRes.error.userMessage).toBe('Friendly user message'); + }); + + it('creates UnprocessableEntityError with HTTP 422', () => { + const err = new UnprocessableEntityError('Invalid payload', { field: 'email' }); + expect(err.statusCode).toBe(422); + expect(err.code).toBe('UNPROCESSABLE_ENTITY'); + expect(err.details).toEqual({ field: 'email' }); + }); + + it('creates BadGatewayError with HTTP 502', () => { + const err = new BadGatewayError('Upstream service failed'); + expect(err.statusCode).toBe(502); + expect(err.code).toBe('BAD_GATEWAY'); + }); + + it('identifies DomainError using isDomainError type guard', () => { + const domainErr = new DomainError('Domain error'); + const standardErr = new Error('Standard error'); + + expect(isDomainError(domainErr)).toBe(true); + expect(isDomainError(standardErr)).toBe(false); + expect(isDomainError(null)).toBe(false); + }); + + it('converts unknown error using fromUnknownError helper', () => { + const nativeErr = new Error('Something broke'); + const wrapped = fromUnknownError(nativeErr, 'WRAPPED_CODE', 500, 'req_999'); + + expect(wrapped).toBeInstanceOf(DomainError); + expect(wrapped.message).toBe('Something broke'); + expect(wrapped.code).toBe('WRAPPED_CODE'); + expect(wrapped.requestId).toBe('req_999'); + }); +}); diff --git a/backend/services/shared/__tests__/kycService.test.ts b/backend/services/shared/__tests__/kycService.test.ts new file mode 100644 index 00000000..3d50730b --- /dev/null +++ b/backend/services/shared/__tests__/kycService.test.ts @@ -0,0 +1,134 @@ +import { KycService, kycService } from '../kycService'; +import { + VerificationTier, + DocumentType, + MerchantOnboardingFormData, + MerchantDocument, +} from '../../../../src/types/merchant'; + +describe('KycService', () => { + const sampleBusinessInfo: MerchantOnboardingFormData = { + businessName: 'Acme Corp', + businessType: 'LLC', + country: 'USA', + phoneNumber: '+1234567890', + email: 'merchant@acme.com', + }; + + const sampleDocuments: MerchantDocument[] = [ + { + id: 'doc-1', + type: DocumentType.ID_FRONT, + uri: 'file:///path/id_front.png', + uploadedAt: new Date(), + status: 'pending', + }, + { + id: 'doc-2', + type: DocumentType.BUSINESS_LICENSE, + uri: 'file:///path/license.pdf', + uploadedAt: new Date(), + status: 'pending', + }, + ]; + + it('singleton instance should be returned by getInstance', () => { + const instance1 = KycService.getInstance(); + const instance2 = KycService.getInstance(); + expect(instance1).toBe(instance2); + expect(kycService).toBe(instance1); + }); + + it('should submit verification request successfully', async () => { + const request = await kycService.submitVerificationRequest( + 'merchant-101', + sampleBusinessInfo, + sampleDocuments + ); + + expect(request).toBeDefined(); + expect(request.merchantId).toBe('merchant-101'); + expect(request.status).toBe('pending'); + expect(request.documents.length).toBe(2); + }); + + it('should throw error when submitting without required fields', async () => { + await expect( + kycService.submitVerificationRequest('', sampleBusinessInfo, sampleDocuments) + ).rejects.toThrow('Merchant ID is required'); + + await expect( + kycService.submitVerificationRequest( + 'merchant-102', + { ...sampleBusinessInfo, businessName: '' }, + sampleDocuments + ) + ).rejects.toThrow('Business name and email are required for KYC verification'); + + await expect( + kycService.submitVerificationRequest('merchant-102', sampleBusinessInfo, []) + ).rejects.toThrow('At least one document is required for KYC verification'); + }); + + it('should process verification and calculate ENHANCED tier for business license', async () => { + await kycService.submitVerificationRequest( + 'merchant-103', + sampleBusinessInfo, + sampleDocuments + ); + + const result = await kycService.processVerification('merchant-103'); + expect(result.verificationResult.isVerified).toBe(true); + expect(result.verificationResult.tier).toBe(VerificationTier.ENHANCED); + expect(result.verificationResult.limits.monthlyVolume).toBe(1000000); + expect(result.verificationResult.limits.maxTransactions).toBe(10000); + }); + + it('should process verification and calculate BASIC tier for basic ID', async () => { + const basicDocs: MerchantDocument[] = [ + { + id: 'doc-basic', + type: DocumentType.ID_FRONT, + uri: 'file:///path/id_front.png', + uploadedAt: new Date(), + status: 'pending', + }, + ]; + + await kycService.submitVerificationRequest( + 'merchant-104', + sampleBusinessInfo, + basicDocs + ); + + const result = await kycService.processVerification('merchant-104'); + expect(result.verificationResult.tier).toBe(VerificationTier.BASIC); + expect(result.verificationResult.limits.monthlyVolume).toBe(10000); + }); + + it('should allow admin approval and rejection', async () => { + await kycService.submitVerificationRequest( + 'merchant-105', + sampleBusinessInfo, + sampleDocuments + ); + + const approved = await kycService.approveVerification( + 'merchant-105', + VerificationTier.ENHANCED, + 'Approved after compliance review' + ); + expect(approved.isVerified).toBe(true); + expect(approved.reviewerNotes).toBe('Approved after compliance review'); + + const statusAfterApprove = kycService.getVerificationStatus('merchant-105'); + expect(statusAfterApprove?.status).toBe('approved'); + + const rejected = await kycService.rejectVerification( + 'merchant-105', + 'Expired ID document' + ); + expect(rejected.isVerified).toBe(false); + expect(rejected.limits.monthlyVolume).toBe(0); + }); +}); diff --git a/backend/services/shared/authStrategies.ts b/backend/services/shared/authStrategies.ts index 8236803b..618f97c5 100644 --- a/backend/services/shared/authStrategies.ts +++ b/backend/services/shared/authStrategies.ts @@ -11,12 +11,14 @@ export interface AuthUser { export interface IAuthStrategy { readonly name: string; readonly rateLimitTier: 'basic' | 'standard' | 'premium'; + readonly priority?: number; validate(req: Request): Promise; } export class JwtAuthStrategy implements IAuthStrategy { readonly name = 'jwt'; readonly rateLimitTier = 'standard' as const; + readonly priority = 10; async validate(req: Request): Promise { const authHeader = req.headers.authorization; @@ -27,7 +29,6 @@ export class JwtAuthStrategy implements IAuthStrategy { if (token === 'invalid-token') { return null; } - // Standard decoded JWT stub / verification logic return { id: 'user_jwt_123', roles: ['user'], @@ -40,6 +41,7 @@ export class JwtAuthStrategy implements IAuthStrategy { export class ApiKeyAuthStrategy implements IAuthStrategy { readonly name = 'api-key'; readonly rateLimitTier = 'premium' as const; + readonly priority = 20; async validate(req: Request): Promise { const apiKey = req.headers['x-api-key'] || req.query.api_key; @@ -61,6 +63,7 @@ export class ApiKeyAuthStrategy implements IAuthStrategy { export class WalletAuthStrategy implements IAuthStrategy { readonly name = 'wallet'; readonly rateLimitTier = 'basic' as const; + readonly priority = 30; async validate(req: Request): Promise { const walletAddress = req.headers['x-wallet-address'] as string; @@ -70,7 +73,6 @@ export class WalletAuthStrategy implements IAuthStrategy { return null; } - // Stellar / EVM public key & signature verification stub return { id: walletAddress, roles: ['wallet_user'], @@ -80,15 +82,48 @@ export class WalletAuthStrategy implements IAuthStrategy { } } +export class OAuthSessionAuthStrategy implements IAuthStrategy { + readonly name = 'oauth-session'; + readonly rateLimitTier = 'standard' as const; + readonly priority = 15; + + async validate(req: Request): Promise { + const sessionCookie = req.headers['x-session-id'] || (req as any).cookies?.sessionId; + if (!sessionCookie || typeof sessionCookie !== 'string') { + return null; + } + if (sessionCookie === 'invalid-session') { + return null; + } + return { + id: `oauth_user_${sessionCookie.slice(0, 8)}`, + roles: ['oauth_user', 'user'], + strategy: this.name, + metadata: { session: sessionCookie.slice(0, 6) + '***' }, + }; + } +} + export class CompositeAuthStrategyManager { private strategies: IAuthStrategy[] = []; constructor(initialStrategies: IAuthStrategy[] = []) { - this.strategies = initialStrategies; + this.strategies = [...initialStrategies].sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100)); } registerStrategy(strategy: IAuthStrategy): void { this.strategies.push(strategy); + this.strategies.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100)); + } + + unregisterStrategy(name: string): boolean { + const initialLen = this.strategies.length; + this.strategies = this.strategies.filter((s) => s.name !== name); + return this.strategies.length < initialLen; + } + + getStrategies(): readonly IAuthStrategy[] { + return [...this.strategies]; } async authenticate(req: Request): Promise { @@ -99,7 +134,6 @@ export class CompositeAuthStrategyManager { return user; } } catch (err) { - // Fallback to next strategy if execution fails continue; } } @@ -119,3 +153,30 @@ export function createUnifiedAuthMiddleware(manager: CompositeAuthStrategyManage } }; } + +export function createRequireRoleMiddleware(allowedRoles: string[]) { + return (req: Request, res: Response, next: NextFunction) => { + const user: AuthUser | undefined = (req as any).user; + if (!user) { + return next(new UnauthorizedError('Authentication required')); + } + const hasRole = user.roles.some((role) => allowedRoles.includes(role)); + if (!hasRole) { + return next(new ForbiddenError(`User lacks required role (${allowedRoles.join(', ')})`)); + } + next(); + }; +} + +export function createRequireStrategyMiddleware(allowedStrategies: string[]) { + return (req: Request, res: Response, next: NextFunction) => { + const user: AuthUser | undefined = (req as any).user; + if (!user) { + return next(new UnauthorizedError('Authentication required')); + } + if (!allowedStrategies.includes(user.strategy)) { + return next(new ForbiddenError(`Authentication strategy '${user.strategy}' not allowed for this route`)); + } + next(); + }; +} diff --git a/backend/services/shared/errors.ts b/backend/services/shared/errors.ts index 40f45601..1d413139 100644 --- a/backend/services/shared/errors.ts +++ b/backend/services/shared/errors.ts @@ -1,5 +1,15 @@ import { ErrorCode } from './apiResponse'; +export interface StructuredErrorDetails { + code: string; + message: string; + userMessage: string; + recovery?: string | null; + details?: Record | null; + timestamp: string; + requestId?: string | null; +} + export class DomainError extends Error { readonly timestamp: string; @@ -8,20 +18,31 @@ export class DomainError extends Error { message: string, public readonly details?: Record, public readonly statusCode: number = 400, - public readonly requestId?: string + public readonly requestId?: string, + public readonly userMessage?: string, + public readonly recovery?: string, + public readonly cause?: unknown ) { - super(message); + const fullMessage = + cause instanceof Error ? `${message} (Caused by: ${cause.message})` : message; + super(fullMessage); this.name = this.constructor.name; this.timestamp = new Date().toISOString(); Object.setPrototypeOf(this, new.target.prototype); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } } - toApiResponse() { + toApiResponse(): { success: false; error: StructuredErrorDetails } { return { success: false, error: { code: this.code, message: this.message, + userMessage: this.userMessage || this.message, + recovery: this.recovery || null, details: this.details || null, timestamp: this.timestamp, requestId: this.requestId || null, @@ -31,49 +52,135 @@ export class DomainError extends Error { } export class ValidationError extends DomainError { - constructor(message: string, details?: Record, requestId?: string) { - super(ErrorCode.INVALID_INPUT, message, details, 400, requestId); + constructor( + message: string, + details?: Record, + requestId?: string, + userMessage?: string, + recovery?: string + ) { + super(ErrorCode.INVALID_INPUT, message, details, 400, requestId, userMessage, recovery); } } export class UnauthorizedError extends DomainError { - constructor(message = 'Unauthorized access', details?: Record, requestId?: string) { - super(ErrorCode.UNAUTHORIZED, message, details, 401, requestId); + constructor( + message = 'Unauthorized access', + details?: Record, + requestId?: string, + userMessage?: string, + recovery?: string + ) { + super(ErrorCode.UNAUTHORIZED, message, details, 401, requestId, userMessage, recovery); } } export class ForbiddenError extends DomainError { - constructor(message = 'Access forbidden', details?: Record, requestId?: string) { - super(ErrorCode.FORBIDDEN, message, details, 403, requestId); + constructor( + message = 'Access forbidden', + details?: Record, + requestId?: string, + userMessage?: string, + recovery?: string + ) { + super(ErrorCode.FORBIDDEN, message, details, 403, requestId, userMessage, recovery); } } export class NotFoundError extends DomainError { - constructor(message = 'Resource not found', details?: Record, requestId?: string) { - super(ErrorCode.NOT_FOUND, message, details, 404, requestId); + constructor( + message = 'Resource not found', + details?: Record, + requestId?: string, + userMessage?: string, + recovery?: string + ) { + super(ErrorCode.NOT_FOUND, message, details, 404, requestId, userMessage, recovery); } } export class ConflictError extends DomainError { - constructor(message = 'Resource conflict', details?: Record, requestId?: string) { - super(ErrorCode.CONFLICT, message, details, 409, requestId); + constructor( + message = 'Resource conflict', + details?: Record, + requestId?: string, + userMessage?: string, + recovery?: string + ) { + super(ErrorCode.CONFLICT, message, details, 409, requestId, userMessage, recovery); + } +} + +export class UnprocessableEntityError extends DomainError { + constructor( + message = 'Unprocessable entity', + details?: Record, + requestId?: string, + userMessage?: string, + recovery?: string + ) { + super('UNPROCESSABLE_ENTITY', message, details, 422, requestId, userMessage, recovery); } } export class RateLimitExceededError extends DomainError { - constructor(message = 'Too many requests', details?: Record, requestId?: string) { - super(ErrorCode.RATE_LIMIT_EXCEEDED, message, details, 429, requestId); + constructor( + message = 'Too many requests', + details?: Record, + requestId?: string, + userMessage?: string, + recovery?: string + ) { + super(ErrorCode.RATE_LIMIT_EXCEEDED, message, details, 429, requestId, userMessage, recovery); } } export class InternalServerError extends DomainError { - constructor(message = 'Internal server error', details?: Record, requestId?: string) { - super(ErrorCode.INTERNAL_SERVER_ERROR, message, details, 500, requestId); + constructor( + message = 'Internal server error', + details?: Record, + requestId?: string, + userMessage?: string, + recovery?: string + ) { + super(ErrorCode.INTERNAL_SERVER_ERROR, message, details, 500, requestId, userMessage, recovery); + } +} + +export class BadGatewayError extends DomainError { + constructor( + message = 'Bad gateway', + details?: Record, + requestId?: string, + userMessage?: string, + recovery?: string + ) { + super('BAD_GATEWAY', message, details, 502, requestId, userMessage, recovery); } } export class ServiceUnavailableError extends DomainError { - constructor(message = 'Service unavailable', details?: Record, requestId?: string) { - super(ErrorCode.SERVICE_UNAVAILABLE, message, details, 533, requestId); + constructor( + message = 'Service unavailable', + details?: Record, + requestId?: string, + userMessage?: string, + recovery?: string + ) { + super(ErrorCode.SERVICE_UNAVAILABLE, message, details, 533, requestId, userMessage, recovery); + } +} + +export function isDomainError(error: unknown): error is DomainError { + return error instanceof DomainError; +} + +export function fromUnknownError(err: unknown, requestId?: string): DomainError { + if (isDomainError(err)) { + return err; + } + if (err instanceof Error) { + return new InternalServerError(err.message, undefined, requestId, undefined, undefined); } + return new InternalServerError(String(err), undefined, requestId, undefined, undefined); } diff --git a/backend/services/shared/index.ts b/backend/services/shared/index.ts index 7d632391..0fc55185 100644 --- a/backend/services/shared/index.ts +++ b/backend/services/shared/index.ts @@ -169,3 +169,8 @@ export type { LeakRecord, PoolTuningRecommendation, } from './poolMonitor'; + +export { KycService, kycService } from './kycService'; +export type { KycVerificationOptions, ProcessingResult } from './kycService'; + + diff --git a/backend/services/shared/kycService.ts b/backend/services/shared/kycService.ts new file mode 100644 index 00000000..0de66ba8 --- /dev/null +++ b/backend/services/shared/kycService.ts @@ -0,0 +1,183 @@ +/** + * KYC Verification Service (Shared Backend Service) + * + * Provides functions for processing merchant KYC verification requests, + * validating document uploads, assigning verification tiers, and managing status. + */ + +import { + MerchantDocument, + MerchantOnboardingFormData, + VerificationResult, + VerificationTier, + OnboardingStatus, + KycVerificationRequest, +} from '../../../src/types/merchant'; + +export interface KycVerificationOptions { + autoApproveBasic?: boolean; + manualReviewRequired?: boolean; +} + +export interface ProcessingResult { + request: KycVerificationRequest; + verificationResult: VerificationResult; + status: OnboardingStatus; +} + +export class KycService { + private static instance: KycService; + private requests: Map = new Map(); + + private constructor() {} + + public static getInstance(): KycService { + if (!KycService.instance) { + KycService.instance = new KycService(); + } + return KycService.instance; + } + + /** + * Submit a new KYC verification request for a merchant. + */ + public async submitVerificationRequest( + merchantId: string, + businessInfo: MerchantOnboardingFormData, + documents: MerchantDocument[] + ): Promise { + if (!merchantId) { + throw new Error('Merchant ID is required'); + } + if (!businessInfo.businessName || !businessInfo.email) { + throw new Error('Business name and email are required for KYC verification'); + } + if (!documents || documents.length === 0) { + throw new Error('At least one document is required for KYC verification'); + } + + const request: KycVerificationRequest = { + merchantId, + documents, + businessInfo, + submittedAt: new Date(), + status: 'pending', + }; + + this.requests.set(merchantId, request); + return request; + } + + /** + * Process a KYC request and calculate the verification tier and volume limits. + */ + public async processVerification( + merchantId: string, + options: KycVerificationOptions = {} + ): Promise { + const request = this.requests.get(merchantId); + if (!request) { + throw new Error(`No KYC request found for merchant ID: ${merchantId}`); + } + + const approvedDocs = request.documents.filter((doc) => doc.status !== 'rejected'); + const hasBusinessLicense = approvedDocs.some( + (doc) => doc.type === 'business_license' || doc.type === 'tax_document' + ); + + // Determine verification tier based on document depth + const tier = hasBusinessLicense ? VerificationTier.ENHANCED : VerificationTier.BASIC; + + const limits = + tier === VerificationTier.ENHANCED + ? { monthlyVolume: 1000000, maxTransactions: 10000 } + : { monthlyVolume: 10000, maxTransactions: 100 }; + + const isVerified = !options.manualReviewRequired && approvedDocs.length >= 1; + const status = isVerified + ? OnboardingStatus.VERIFIED + : OnboardingStatus.PENDING_REVIEW; + + const verificationResult: VerificationResult = { + isVerified, + tier, + reviewedAt: new Date(), + reviewerNotes: isVerified + ? `Automated verification completed for tier ${tier}` + : 'Pending manual compliance review', + limits, + }; + + request.status = isVerified ? 'approved' : 'in_review'; + this.requests.set(merchantId, request); + + return { + request, + verificationResult, + status, + }; + } + + /** + * Admin approve a pending KYC verification request. + */ + public async approveVerification( + merchantId: string, + tier: VerificationTier = VerificationTier.BASIC, + notes?: string + ): Promise { + const request = this.requests.get(merchantId); + if (!request) { + throw new Error(`No KYC request found for merchant ID: ${merchantId}`); + } + + request.status = 'approved'; + request.reviewNotes = notes; + + const limits = + tier === VerificationTier.ENHANCED + ? { monthlyVolume: 1000000, maxTransactions: 10000 } + : { monthlyVolume: 10000, maxTransactions: 100 }; + + return { + isVerified: true, + tier, + reviewedAt: new Date(), + reviewerNotes: notes || `Approved at ${tier} tier`, + limits, + }; + } + + /** + * Admin reject a pending KYC verification request. + */ + public async rejectVerification( + merchantId: string, + reason: string + ): Promise { + const request = this.requests.get(merchantId); + if (!request) { + throw new Error(`No KYC request found for merchant ID: ${merchantId}`); + } + + request.status = 'rejected'; + request.reviewNotes = reason; + + return { + isVerified: false, + tier: VerificationTier.BASIC, + reviewedAt: new Date(), + reviewerNotes: reason, + limits: { monthlyVolume: 0, maxTransactions: 0 }, + }; + } + + /** + * Get the current KYC request status for a merchant. + */ + public getVerificationStatus(merchantId: string): KycVerificationRequest | undefined { + return this.requests.get(merchantId); + } +} + +export const kycService = KycService.getInstance(); diff --git a/contracts/subscription/Cargo.toml b/contracts/subscription/Cargo.toml index 6e8642a2..c54071c6 100644 --- a/contracts/subscription/Cargo.toml +++ b/contracts/subscription/Cargo.toml @@ -10,13 +10,12 @@ name = "subtrackr_subscription" path = "src/lib.rs" crate-type = ["cdylib", "rlib"] -serde = "1.0" - [dependencies] -soroban-sdk = "21.0.0" +soroban-sdk = { workspace = true } subtrackr-types = { path = "../types" } +serde = { version = "1.0", default-features = false } [dev-dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { workspace = true, features = ["testutils"] } subtrackr-proxy = { path = "../proxy" } subtrackr-storage = { path = "../storage" } diff --git a/contracts/subscription/src/gas_optimization.rs b/contracts/subscription/src/gas_optimization.rs index e4f1434e..73d291c4 100644 --- a/contracts/subscription/src/gas_optimization.rs +++ b/contracts/subscription/src/gas_optimization.rs @@ -1,6 +1,3 @@ -/// Gas Optimization and Targeting Module -/// Provides optimization recommendations and tracks gas targets -#![allow(dead_code)] //! Gas Optimization and Targeting Module //! Provides optimization recommendations and tracks gas targets. diff --git a/contracts/subscription/src/gas_profiler.rs b/contracts/subscription/src/gas_profiler.rs index 9f308199..e3ece2bd 100644 --- a/contracts/subscription/src/gas_profiler.rs +++ b/contracts/subscription/src/gas_profiler.rs @@ -1,11 +1,7 @@ -/// Gas Profiling Module for SubTrackr Subscription Contract -/// Tracks gas consumption for each contract function and provides optimization insights -use soroban_sdk::{Address, Env, String, Symbol, Vec}; -#![allow(dead_code)] -#![allow(unused_variables)] //! Gas Profiling Module for SubTrackr Subscription Contract //! Tracks gas consumption for each contract function and provides optimization insights. -use soroban_sdk::{Address, Env, String, Vec}; + +use soroban_sdk::{Address, Env, String, Symbol, Vec}; /// Gas profile entry for a function call #[derive(Clone)] diff --git a/contracts/subscription/src/gas_storage.rs b/contracts/subscription/src/gas_storage.rs index a14e8e43..db27cbb4 100644 --- a/contracts/subscription/src/gas_storage.rs +++ b/contracts/subscription/src/gas_storage.rs @@ -1,13 +1,8 @@ -use crate::gas_profiler::GasProfile; -/// Gas Storage Module -/// Manages storage and retrieval of gas profiling metrics -use soroban_sdk::{Address, Env, IntoVal, String as SorobanString, TryFromVal, Val, Vec}; -#![allow(dead_code)] -#![allow(unused_variables)] //! Gas Storage Module //! Manages storage and retrieval of gas profiling metrics. -use soroban_sdk::{Address, Env, String as SorobanString}; -use crate::gas_profiler::{GasProfile}; + +use crate::gas_profiler::GasProfile; +use soroban_sdk::{Address, Env, IntoVal, String as SorobanString, TryFromVal, Val, Vec}; /// Storage keys for gas metrics #[derive(Clone)] diff --git a/contracts/subscription/src/lib.rs b/contracts/subscription/src/lib.rs index 7a15210b..d014bace 100644 --- a/contracts/subscription/src/lib.rs +++ b/contracts/subscription/src/lib.rs @@ -1,14 +1,42 @@ #![no_std] -mod gas_optimization; -mod gas_profiler; -mod gas_storage; -mod quota; -mod revenue; -mod usage; + +pub mod admin; +pub mod billing; +pub mod cancellation; +pub mod charging; +pub mod errors; +pub mod event_store; +pub mod events; +pub mod gas_benchmarks; +pub mod gas_optimization; +pub mod gas_profiler; +pub mod gas_storage; +pub mod invoice_branding; +pub mod loyalty; +pub mod payment; +pub mod payment_methods; +pub mod plan; +pub mod plan_templates; +pub mod proration; +pub mod quota; +pub mod reentrancy; +pub mod retention; +pub mod revenue; +pub mod state; +pub mod subscription_lifecycle; +pub mod subtrackr_subscription; +pub mod timeout; +pub mod usage; +pub mod webhook; + +pub const MAX_PLANS_PER_MERCHANT: u32 = 100; + +pub use subtrackr_subscription::{SubTrackrSubscription, SubTrackrSubscriptionClient}; + use soroban_sdk::{token, Address, Bytes, BytesN, Env, IntoVal, String, TryFromVal, Val, Vec}; use subtrackr_types::{ - ChargeCommitment, Interval, Invoice, MevAlert, MevProtectionConfig, Plan, StorageKey, - Subscription, SubscriptionStatus, TimeRange, + ChargeCommitment, Invoice, MevAlert, MevProtectionConfig, Plan, StorageKey, Subscription, + SubscriptionStatus, TimeRange, }; /// Billing interval in seconds. @@ -391,1018 +419,3 @@ fn charge_subscription_guarded( let _ = _invoice; } } - -// ───────────────────────────────────────────────────────────────────────────── -// Implementation Contract -// ───────────────────────────────────────────────────────────────────────────── - -#[soroban_sdk::contract] -pub struct SubTrackrSubscription; - -#[soroban_sdk::contractimpl] -impl SubTrackrSubscription { - // ── Upgrade interface ── - - pub fn get_version(_env: Env, proxy: Address, _storage: Address) -> u32 { - proxy.require_auth(); - STORAGE_VERSION - } - - pub fn validate_upgrade(env: Env, proxy: Address, storage: Address, from_version: u32) { - proxy.require_auth(); - assert!(from_version > 0, "Invalid version"); - assert!( - from_version <= STORAGE_VERSION, - "Cannot upgrade from future version" - ); - - // Ensure core keys exist before allowing upgrade/migration. - let _admin: Address = get_admin(&env, &storage); - let _plan_count: u64 = - storage_instance_get(&env, &storage, StorageKey::PlanCount).unwrap_or(0); - let _sub_count: u64 = - storage_instance_get(&env, &storage, StorageKey::SubscriptionCount).unwrap_or(0); - } - - /// Migrate storage from `from_version` to this implementation's `STORAGE_VERSION`. - /// - /// For v1 -> v2: build `UserPlanIndex` for all active/non-cancelled subscriptions. - pub fn migrate(env: Env, proxy: Address, storage: Address, from_version: u32) { - proxy.require_auth(); - if from_version == STORAGE_VERSION { - return; - } - assert!(from_version < STORAGE_VERSION, "Unsupported migration path"); - - if from_version == 1 { - let sub_count: u64 = - storage_instance_get(&env, &storage, StorageKey::SubscriptionCount).unwrap_or(0); - let mut i: u64 = 1; - while i <= sub_count { - let sub_opt: Option = - storage_persistent_get(&env, &storage, StorageKey::Subscription(i)); - if let Some(sub) = sub_opt { - if sub.status != SubscriptionStatus::Cancelled { - set_user_plan_index(&env, &storage, &sub.subscriber, sub.plan_id, sub.id); - } - } - i += 1; - } - return; - } - - panic!("Unsupported migration path"); - } - - // ── Initialization ── - - pub fn initialize(env: Env, proxy: Address, storage: Address, admin: Address) { - proxy.require_auth(); - admin.require_auth(); - - storage_instance_set(&env, &storage, StorageKey::Admin, admin); - storage_instance_set(&env, &storage, StorageKey::PlanCount, 0u64); - storage_instance_set(&env, &storage, StorageKey::SubscriptionCount, 0u64); - storage_instance_remove(&env, &storage, StorageKey::InvoiceContract); - } - - pub fn set_invoice_contract(env: Env, proxy: Address, storage: Address, invoice: Address) { - proxy.require_auth(); - let admin = get_admin(&env, &storage); - admin.require_auth(); - storage_instance_set(&env, &storage, StorageKey::InvoiceContract, invoice); - } - - pub fn clear_invoice_contract(env: Env, proxy: Address, storage: Address) { - proxy.require_auth(); - let admin = get_admin(&env, &storage); - admin.require_auth(); - storage_instance_remove(&env, &storage, StorageKey::InvoiceContract); - } - - // ── Rate Limiting Admin ── - - pub fn set_rate_limit( - env: Env, - proxy: Address, - storage: Address, - function: String, - min_interval_secs: u64, - ) { - proxy.require_auth(); - let admin = get_admin(&env, &storage); - admin.require_auth(); - storage_instance_set( - &env, - &storage, - StorageKey::RateLimit(function), - min_interval_secs, - ); - } - - pub fn remove_rate_limit(env: Env, proxy: Address, storage: Address, function: String) { - proxy.require_auth(); - let admin = get_admin(&env, &storage); - admin.require_auth(); - storage_instance_remove(&env, &storage, StorageKey::RateLimit(function)); - } - - pub fn configure_mev_protection( - env: Env, - proxy: Address, - storage: Address, - admin: Address, - config: MevProtectionConfig, - ) { - proxy.require_auth(); - assert!(admin == get_admin(&env, &storage), "Admin mismatch"); - admin.require_auth(); - validate_mev_config(&config); - storage_instance_set( - &env, - &storage, - StorageKey::MevProtectionConfig, - config.clone(), - ); - env.events().publish( - (String::from_str(&env, "mev_configured"), admin), - ( - config.large_charge_threshold, - config.max_fee_bps, - config.private_mempool_required, - config.gas_price_alert_threshold, - ), - ); - } - - pub fn commit_charge( - env: Env, - proxy: Address, - storage: Address, - subscription_id: u64, - commitment: BytesN<32>, - ) { - proxy.require_auth(); - let sub: Subscription = - storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) - .expect("Subscription not found"); - sub.subscriber.require_auth(); - - let now = env.ledger().timestamp(); - if let Some(existing) = storage_persistent_get::( - &env, - &storage, - StorageKey::ChargeCommitment(subscription_id), - ) { - assert!( - now > existing.expires_at, - "Pending charge commitment exists" - ); - } - - let config = get_mev_config(&env, &storage); - let pending = ChargeCommitment { - subscription_id, - subscriber: sub.subscriber.clone(), - commitment: commitment.clone(), - committed_at: now, - min_reveal_at: now + config.reveal_delay_secs, - expires_at: now + config.commit_ttl_secs, - }; - storage_persistent_set( - &env, - &storage, - StorageKey::ChargeCommitment(subscription_id), - pending.clone(), - ); - env.events().publish( - (String::from_str(&env, "charge_committed"), subscription_id), - ( - pending.subscriber, - pending.min_reveal_at, - pending.expires_at, - ), - ); - } - - pub fn hash_charge_commitment( - env: Env, - _proxy: Address, - _storage: Address, - subscription_id: u64, - max_charge_amount: i128, - salt: BytesN<32>, - ) -> BytesN<32> { - build_charge_commitment(&env, subscription_id, max_charge_amount, &salt) - } - - pub fn reveal_charge( - env: Env, - proxy: Address, - storage: Address, - subscription_id: u64, - salt: BytesN<32>, - max_charge_amount: i128, - observed_gas_price: u64, - private_mempool: bool, - ) { - proxy.require_auth(); - let pending: ChargeCommitment = storage_persistent_get( - &env, - &storage, - StorageKey::ChargeCommitment(subscription_id), - ) - .expect("Charge commitment not found"); - - let now = env.ledger().timestamp(); - assert!(now >= pending.min_reveal_at, "Reveal delay not met"); - assert!(now <= pending.expires_at, "Charge commitment expired"); - let revealed_commitment = - build_charge_commitment(&env, subscription_id, max_charge_amount, &salt); - assert!( - pending.commitment == revealed_commitment, - "Commitment mismatch" - ); - pending.subscriber.require_auth(); - - storage_persistent_remove( - &env, - &storage, - StorageKey::ChargeCommitment(subscription_id), - ); - env.events().publish( - (String::from_str(&env, "charge_revealed"), subscription_id), - (max_charge_amount, observed_gas_price, private_mempool, now), - ); - - charge_subscription_guarded( - &env, - &storage, - subscription_id, - max_charge_amount, - observed_gas_price, - private_mempool, - true, - ); - } - - // ── Plan Management ── - - pub fn create_plan( - env: Env, - proxy: Address, - storage: Address, - merchant: Address, - name: String, - price: i128, - token: Address, - interval: Interval, - ) -> u64 { - proxy.require_auth(); - if merchant != get_admin(&env, &storage) { - enforce_rate_limit(&env, &storage, &merchant, "create_plan"); - } - merchant.require_auth(); - assert!(price > 0, "Price must be positive"); - - let mut count: u64 = - storage_instance_get(&env, &storage, StorageKey::PlanCount).unwrap_or(0); - count += 1; - - let plan = Plan { - id: count, - merchant: merchant.clone(), - name, - price, - token, - interval, - active: true, - subscriber_count: 0, - created_at: env.ledger().timestamp(), - }; - - storage_persistent_set(&env, &storage, StorageKey::Plan(count), plan.clone()); - storage_instance_set(&env, &storage, StorageKey::PlanCount, count); - - let mut merchant_plans: Vec = - storage_persistent_get(&env, &storage, StorageKey::MerchantPlans(merchant.clone())) - .unwrap_or(Vec::new(&env)); - merchant_plans.push_back(count); - storage_persistent_set( - &env, - &storage, - StorageKey::MerchantPlans(merchant), - merchant_plans, - ); - - count - } - - pub fn deactivate_plan( - env: Env, - proxy: Address, - storage: Address, - merchant: Address, - plan_id: u64, - ) { - proxy.require_auth(); - if merchant != get_admin(&env, &storage) { - enforce_rate_limit(&env, &storage, &merchant, "deactivate_plan"); - } - merchant.require_auth(); - - let mut plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(plan_id)) - .expect("Plan not found"); - - assert!(plan.merchant == merchant, "Only plan owner can deactivate"); - plan.active = false; - - storage_persistent_set(&env, &storage, StorageKey::Plan(plan_id), plan); - } - - // ── Subscription Management ── - - pub fn subscribe( - env: Env, - proxy: Address, - storage: Address, - subscriber: Address, - plan_id: u64, - ) -> u64 { - proxy.require_auth(); - if subscriber != get_admin(&env, &storage) { - enforce_rate_limit(&env, &storage, &subscriber, "subscribe"); - } - subscriber.require_auth(); - - let mut plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(plan_id)) - .expect("Plan not found"); - assert!(plan.active, "Plan is not active"); - assert!( - plan.merchant != subscriber, - "Merchant cannot self-subscribe" - ); - - if let Some(existing_id) = get_user_plan_index(&env, &storage, &subscriber, plan_id) { - let existing_sub: Subscription = - storage_persistent_get(&env, &storage, StorageKey::Subscription(existing_id)) - .expect("Subscription not found"); - if existing_sub.status != SubscriptionStatus::Cancelled { - panic!("Already subscribed to this plan"); - } - } - - let mut sub_count: u64 = - storage_instance_get(&env, &storage, StorageKey::SubscriptionCount).unwrap_or(0); - sub_count += 1; - - let now = env.ledger().timestamp(); - - let subscription = Subscription { - id: sub_count, - plan_id, - subscriber: subscriber.clone(), - status: SubscriptionStatus::Active, - started_at: now, - last_charged_at: now, - next_charge_at: now + plan.interval.seconds(), - total_paid: 0, - total_gas_spent: 0, - charge_count: 0, - paused_at: 0, - pause_duration: 0, - refund_requested_amount: 0, - }; - - storage_persistent_set( - &env, - &storage, - StorageKey::Subscription(sub_count), - subscription, - ); - storage_instance_set(&env, &storage, StorageKey::SubscriptionCount, sub_count); - - let mut user_subs: Vec = storage_persistent_get( - &env, - &storage, - StorageKey::UserSubscriptions(subscriber.clone()), - ) - .unwrap_or(Vec::new(&env)); - user_subs.push_back(sub_count); - storage_persistent_set( - &env, - &storage, - StorageKey::UserSubscriptions(subscriber.clone()), - user_subs, - ); - - // Index for quick duplicate checks - set_user_plan_index(&env, &storage, &subscriber, plan_id, sub_count); - - plan.subscriber_count += 1; - storage_persistent_set(&env, &storage, StorageKey::Plan(plan_id), plan); - - sub_count - } - - pub fn cancel_subscription( - env: Env, - proxy: Address, - storage: Address, - subscriber: Address, - subscription_id: u64, - ) { - proxy.require_auth(); - if subscriber != get_admin(&env, &storage) { - enforce_rate_limit(&env, &storage, &subscriber, "cancel_subscription"); - } - subscriber.require_auth(); - - let mut sub: Subscription = - storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) - .expect("Subscription not found"); - - assert!(sub.subscriber == subscriber, "Only subscriber can cancel"); - assert!( - sub.status == SubscriptionStatus::Active || sub.status == SubscriptionStatus::Paused, - "Subscription not active" - ); - - sub.status = SubscriptionStatus::Cancelled; - storage_persistent_set( - &env, - &storage, - StorageKey::Subscription(subscription_id), - sub.clone(), - ); - - // Remove index - remove_user_plan_index(&env, &storage, &subscriber, sub.plan_id); - - let mut plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(sub.plan_id)) - .expect("Plan not found"); - if plan.subscriber_count > 0 { - plan.subscriber_count -= 1; - } - storage_persistent_set(&env, &storage, StorageKey::Plan(sub.plan_id), plan); - } - - pub fn pause_subscription( - env: Env, - proxy: Address, - storage: Address, - subscriber: Address, - subscription_id: u64, - ) { - proxy.require_auth(); - if subscriber != get_admin(&env, &storage) { - enforce_rate_limit(&env, &storage, &subscriber, "pause_subscription"); - } - Self::pause_by_subscriber( - env, - proxy, - storage, - subscriber, - subscription_id, - MAX_PAUSE_DURATION, - ); - } - - pub fn pause_by_subscriber( - env: Env, - proxy: Address, - storage: Address, - subscriber: Address, - subscription_id: u64, - duration: u64, - ) { - proxy.require_auth(); - if subscriber != get_admin(&env, &storage) { - enforce_rate_limit(&env, &storage, &subscriber, "pause_by_subscriber"); - } - subscriber.require_auth(); - - let mut sub: Subscription = - storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) - .expect("Subscription not found"); - - assert!(sub.subscriber == subscriber, "Only subscriber can pause"); - assert!( - sub.status == SubscriptionStatus::Active, - "Only active subscriptions can be paused" - ); - assert!( - duration <= MAX_PAUSE_DURATION, - "Pause duration exceeds limit" - ); - - sub.status = SubscriptionStatus::Paused; - sub.paused_at = env.ledger().timestamp(); - sub.pause_duration = duration; - - storage_persistent_set( - &env, - &storage, - StorageKey::Subscription(subscription_id), - sub.clone(), - ); - - env.events().publish( - (String::from_str(&env, "subscription_paused"), subscriber), - (subscription_id, sub.paused_at, duration), - ); - } - - pub fn resume_subscription( - env: Env, - proxy: Address, - storage: Address, - subscriber: Address, - subscription_id: u64, - ) { - proxy.require_auth(); - if subscriber != get_admin(&env, &storage) { - enforce_rate_limit(&env, &storage, &subscriber, "resume_subscription"); - } - subscriber.require_auth(); - - let mut sub: Subscription = - storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) - .expect("Subscription not found"); - - assert!(sub.subscriber == subscriber, "Only subscriber can resume"); - assert!( - sub.status == SubscriptionStatus::Paused || check_and_resume_internal(&env, &mut sub), - "Only paused subscriptions can be resumed" - ); - - let now = env.ledger().timestamp(); - let plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(sub.plan_id)) - .expect("Plan not found"); - - sub.status = SubscriptionStatus::Active; - sub.next_charge_at = now + plan.interval.seconds(); - sub.paused_at = 0; - sub.pause_duration = 0; - - storage_persistent_set( - &env, - &storage, - StorageKey::Subscription(subscription_id), - sub, - ); - - env.events().publish( - (String::from_str(&env, "subscription_resumed"), subscriber), - subscription_id, - ); - } - - // ── Payment Processing ── - - pub fn charge_subscription(env: Env, proxy: Address, storage: Address, subscription_id: u64) { - proxy.require_auth(); - charge_subscription_guarded(&env, &storage, subscription_id, i128::MAX, 0, false, false); - } - - pub fn request_refund( - env: Env, - proxy: Address, - storage: Address, - subscription_id: u64, - amount: i128, - ) { - proxy.require_auth(); - let mut sub: Subscription = - storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) - .expect("Subscription not found"); - - if sub.subscriber != get_admin(&env, &storage) { - enforce_rate_limit(&env, &storage, &sub.subscriber, "request_refund"); - } - - sub.subscriber.require_auth(); - - assert!(amount > 0, "Refund amount must be positive"); - assert!( - amount <= sub.total_paid, - "Refund amount cannot exceed total paid" - ); - - sub.refund_requested_amount = amount; - storage_persistent_set( - &env, - &storage, - StorageKey::Subscription(subscription_id), - sub.clone(), - ); - - env.events().publish( - (String::from_str(&env, "refund_requested"), subscription_id), - (sub.subscriber.clone(), amount), - ); - } - - pub fn approve_refund(env: Env, proxy: Address, storage: Address, subscription_id: u64) { - proxy.require_auth(); - let mut sub: Subscription = - storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) - .expect("Subscription not found"); - - let admin = get_admin(&env, &storage); - admin.require_auth(); - - let amount = sub.refund_requested_amount; - assert!(amount > 0, "No pending refund request"); - - let _plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(sub.plan_id)) - .expect("Plan not found"); - - sub.total_paid -= amount; - sub.refund_requested_amount = 0; - - storage_persistent_set( - &env, - &storage, - StorageKey::Subscription(subscription_id), - sub.clone(), - ); - - env.events().publish( - (String::from_str(&env, "refund_approved"), subscription_id), - (sub.subscriber.clone(), amount), - ); - } - - pub fn reject_refund(env: Env, proxy: Address, storage: Address, subscription_id: u64) { - proxy.require_auth(); - let mut sub: Subscription = - storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) - .expect("Subscription not found"); - - let admin = get_admin(&env, &storage); - admin.require_auth(); - - assert!(sub.refund_requested_amount > 0, "No pending refund request"); - sub.refund_requested_amount = 0; - - storage_persistent_set( - &env, - &storage, - StorageKey::Subscription(subscription_id), - sub.clone(), - ); - - env.events().publish( - (String::from_str(&env, "refund_rejected"), subscription_id), - sub.subscriber.clone(), - ); - } - - // ── Subscription Transfer ── - - pub fn request_transfer( - env: Env, - proxy: Address, - storage: Address, - subscription_id: u64, - recipient: Address, - ) { - proxy.require_auth(); - let sub: Subscription = - storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) - .expect("Subscription not found"); - - if sub.subscriber != get_admin(&env, &storage) { - enforce_rate_limit(&env, &storage, &sub.subscriber, "request_transfer"); - } - - sub.subscriber.require_auth(); - assert!( - sub.status != SubscriptionStatus::Cancelled, - "Subscription is cancelled" - ); - assert!(sub.subscriber != recipient, "Cannot transfer to self"); - - storage_instance_set( - &env, - &storage, - StorageKey::PendingTransfer(subscription_id), - recipient.clone(), - ); - - env.events().publish( - ( - String::from_str(&env, "transfer_requested"), - subscription_id, - ), - (sub.subscriber.clone(), recipient), - ); - } - - pub fn accept_transfer( - env: Env, - proxy: Address, - storage: Address, - subscription_id: u64, - recipient: Address, - ) { - proxy.require_auth(); - if recipient != get_admin(&env, &storage) { - enforce_rate_limit(&env, &storage, &recipient, "accept_transfer"); - } - recipient.require_auth(); - - let mut sub: Subscription = - storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) - .expect("Subscription not found"); - - let pending_recipient: Address = - storage_instance_get(&env, &storage, StorageKey::PendingTransfer(subscription_id)) - .expect("No pending transfer for this subscription"); - assert!( - pending_recipient == recipient, - "Transfer recipient mismatch" - ); - - let old_user_subs: Vec = storage_persistent_get( - &env, - &storage, - StorageKey::UserSubscriptions(sub.subscriber.clone()), - ) - .unwrap_or(Vec::new(&env)); - let mut new_list: Vec = Vec::new(&env); - for id in old_user_subs.iter() { - if id != subscription_id { - new_list.push_back(id); - } - } - storage_persistent_set( - &env, - &storage, - StorageKey::UserSubscriptions(sub.subscriber.clone()), - new_list, - ); - - let mut rec_user_subs: Vec = storage_persistent_get( - &env, - &storage, - StorageKey::UserSubscriptions(recipient.clone()), - ) - .unwrap_or(Vec::new(&env)); - rec_user_subs.push_back(subscription_id); - storage_persistent_set( - &env, - &storage, - StorageKey::UserSubscriptions(recipient.clone()), - rec_user_subs, - ); - - // Update index mapping - remove_user_plan_index(&env, &storage, &sub.subscriber, sub.plan_id); - set_user_plan_index(&env, &storage, &recipient, sub.plan_id, sub.id); - - let old = sub.subscriber.clone(); - sub.subscriber = recipient.clone(); - storage_persistent_set( - &env, - &storage, - StorageKey::Subscription(subscription_id), - sub, - ); - - storage_instance_remove(&env, &storage, StorageKey::PendingTransfer(subscription_id)); - - env.events().publish( - (String::from_str(&env, "transfer_accepted"), subscription_id), - (old, recipient), - ); - } - - // ── Queries ── - - pub fn get_plan(env: Env, proxy: Address, storage: Address, plan_id: u64) -> Plan { - proxy.require_auth(); - storage_persistent_get(&env, &storage, StorageKey::Plan(plan_id)).expect("Plan not found") - } - - pub fn get_subscription( - env: Env, - proxy: Address, - storage: Address, - subscription_id: u64, - ) -> Subscription { - proxy.require_auth(); - let mut sub: Subscription = - storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) - .expect("Subscription not found"); - - check_and_resume_internal(&env, &mut sub); - sub - } - - pub fn get_user_subscriptions( - env: Env, - proxy: Address, - storage: Address, - subscriber: Address, - ) -> Vec { - proxy.require_auth(); - storage_persistent_get(&env, &storage, StorageKey::UserSubscriptions(subscriber)) - .unwrap_or(Vec::new(&env)) - } - - pub fn get_merchant_plans( - env: Env, - proxy: Address, - storage: Address, - merchant: Address, - ) -> Vec { - proxy.require_auth(); - storage_persistent_get(&env, &storage, StorageKey::MerchantPlans(merchant)) - .unwrap_or(Vec::new(&env)) - } - - pub fn get_plan_count(env: Env, proxy: Address, storage: Address) -> u64 { - proxy.require_auth(); - storage_instance_get(&env, &storage, StorageKey::PlanCount).unwrap_or(0) - } - - pub fn get_subscription_count(env: Env, proxy: Address, storage: Address) -> u64 { - proxy.require_auth(); - storage_instance_get(&env, &storage, StorageKey::SubscriptionCount).unwrap_or(0) - } - - pub fn get_mev_protection_config( - env: Env, - proxy: Address, - storage: Address, - ) -> MevProtectionConfig { - proxy.require_auth(); - get_mev_config(&env, &storage) - } - - pub fn get_charge_commitment( - env: Env, - proxy: Address, - storage: Address, - subscription_id: u64, - ) -> Option { - proxy.require_auth(); - storage_persistent_get( - &env, - &storage, - StorageKey::ChargeCommitment(subscription_id), - ) - } - - pub fn get_mev_alert_count(env: Env, proxy: Address, storage: Address) -> u64 { - proxy.require_auth(); - storage_instance_get(&env, &storage, StorageKey::MevAlertCount).unwrap_or(0) - } - - // ── Revenue Recognition API ── - - /// Set a revenue recognition rule for a plan (merchant only). - pub fn set_revenue_rule( - env: Env, - proxy: Address, - storage: Address, - merchant: Address, - plan_id: u64, - method: revenue::RecognitionMethod, - recognition_period: u64, - ) { - proxy.require_auth(); - merchant.require_auth(); - let plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(plan_id)) - .expect("Plan not found"); - assert!( - plan.merchant == merchant, - "Only plan owner can set revenue rule" - ); - revenue::set_recognition_rule( - &env, - &storage, - revenue::RevenueRecognitionRule { - plan_id, - method, - recognition_period, - }, - ); - } - - /// Compute a recognition snapshot for a subscription as of the current ledger time. - pub fn recognize_revenue( - env: Env, - proxy: Address, - storage: Address, - subscription_id: u64, - ) -> revenue::Recognition { - proxy.require_auth(); - let sub: Subscription = - storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) - .expect("Subscription not found"); - let plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(sub.plan_id)) - .expect("Plan not found"); - let now = env.ledger().timestamp(); - revenue::recognize_revenue(&env, &storage, subscription_id, plan.merchant, now) - } - - /// Return the cumulative deferred revenue balance for a merchant. - pub fn get_deferred_revenue( - env: Env, - proxy: Address, - storage: Address, - merchant_id: Address, - ) -> i128 { - proxy.require_auth(); - revenue::get_deferred_revenue(&env, &storage, &merchant_id) - } - - /// Return the revenue schedule for a subscription (None if not yet generated). - pub fn get_revenue_schedule( - env: Env, - proxy: Address, - storage: Address, - subscription_id: u64, - ) -> Option { - proxy.require_auth(); - revenue::get_revenue_schedule(&env, &storage, subscription_id) - } - - // ── Quota & Usage API ── - - pub fn set_plan_quotas( - env: Env, - proxy: Address, - storage: Address, - merchant: Address, - plan_id: u64, - quotas: Vec, - ) { - proxy.require_auth(); - merchant.require_auth(); - let plan: subtrackr_types::Plan = - storage_persistent_get(&env, &storage, StorageKey::Plan(plan_id)) - .expect("Plan not found"); - assert!(plan.merchant == merchant, "Only plan owner can set quotas"); - quota::set_plan_quotas(&env, &storage, plan_id, quotas); - } - - pub fn get_plan_quotas( - env: Env, - proxy: Address, - storage: Address, - plan_id: u64, - ) -> Vec { - proxy.require_auth(); - quota::get_plan_quotas(&env, &storage, plan_id) - } - - pub fn record_usage( - env: Env, - proxy: Address, - storage: Address, - subscription_id: u64, - metric: subtrackr_types::QuotaMetric, - amount: u64, - ) -> subtrackr_types::UsageRecord { - proxy.require_auth(); - let sub: subtrackr_types::Subscription = - storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) - .expect("Subscription not found"); - - let _admin = get_admin(&env, &storage); - // Only subscriber or admin can record usage? Usually it's the app/admin - // For simplicity, let's allow anyone with auth (simplified for this task) - // In a real app, you might want more complex auth. - - usage::record_usage(&env, &storage, subscription_id, sub.plan_id, metric, amount) - } - - pub fn get_usage_record( - env: Env, - proxy: Address, - storage: Address, - subscription_id: u64, - metric: subtrackr_types::QuotaMetric, - ) -> subtrackr_types::UsageRecord { - proxy.require_auth(); - usage::get_usage_record(&env, &storage, subscription_id, metric) - } - - pub fn check_quota( - env: Env, - proxy: Address, - storage: Address, - subscription_id: u64, - metric: subtrackr_types::QuotaMetric, - ) -> subtrackr_types::QuotaStatus { - proxy.require_auth(); - let sub: subtrackr_types::Subscription = - storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) - .expect("Subscription not found"); - usage::check_quota(&env, &storage, subscription_id, sub.plan_id, metric) - } -} diff --git a/contracts/subscription/src/payment_methods.rs b/contracts/subscription/src/payment_methods.rs index 5420e097..dedec071 100644 --- a/contracts/subscription/src/payment_methods.rs +++ b/contracts/subscription/src/payment_methods.rs @@ -1,9 +1,6 @@ -extern crate alloc; -use alloc::format; -use alloc::string::ToString; - -use soroban_sdk::{token, Address, Env, String, Symbol, Vec}; -use subtrackr_types::{PaymentMethod, PaymentMethodId, PaymentPriority, TokenType}; +use soroban_sdk::{token, Address, Env, String, Vec}; +use subtrackr_types::{PaymentMethod, PaymentMethodId, PaymentPriority, StorageKey, TokenType}; +use crate::{storage_persistent_get, storage_persistent_remove, storage_persistent_set}; const MAX_PAYMENT_METHODS: u32 = 10; const DEFAULT_EXPIRY_WARNING_DAYS: u64 = 30 * 24 * 60 * 60; @@ -16,77 +13,47 @@ fn priority_weight(priority: &PaymentPriority) -> u32 { } } -fn user_method_list_key(env: &Env, user: &Address) -> Symbol { - let formatted = format!("pm_list_{:?}", user); - Symbol::new(env, &formatted) -} - -fn method_key(env: &Env, user: &Address, method_id: PaymentMethodId) -> Symbol { - let formatted = format!("pm_{:?}_{}", user, method_id); - Symbol::new(env, &formatted) -} - -fn user_count_key(env: &Env, user: &Address) -> Symbol { - let formatted = format!("pm_count_{:?}", user); - Symbol::new(env, &formatted) -} - -fn get_user_count(env: &Env, user: &Address) -> u64 { - env.storage() - .persistent() - .get::<_, u64>(&user_count_key(env, user)) +fn get_user_count(env: &Env, storage: &Address, user: &Address) -> u64 { + storage_persistent_get(env, storage, StorageKey::PaymentMethodCount(user.clone())) .unwrap_or(0) } -fn set_user_count(env: &Env, user: &Address, count: u64) { - env.storage() - .persistent() - .set(&user_count_key(env, user), &count); +fn set_user_count(env: &Env, storage: &Address, user: &Address, count: u64) { + storage_persistent_set(env, storage, StorageKey::PaymentMethodCount(user.clone()), count); } -fn get_user_method_ids(env: &Env, user: &Address) -> Vec { - env.storage() - .persistent() - .get::<_, Vec>(&user_method_list_key(env, user)) +fn get_user_method_ids(env: &Env, storage: &Address, user: &Address) -> Vec { + storage_persistent_get(env, storage, StorageKey::UserPaymentMethods(user.clone())) .unwrap_or(Vec::new(env)) } -fn set_user_method_ids(env: &Env, user: &Address, ids: Vec) { - env.storage() - .persistent() - .set(&user_method_list_key(env, user), &ids); +fn set_user_method_ids(env: &Env, storage: &Address, user: &Address, ids: Vec) { + storage_persistent_set(env, storage, StorageKey::UserPaymentMethods(user.clone()), ids); } -fn get_method(env: &Env, user: &Address, method_id: PaymentMethodId) -> Option { - env.storage() - .persistent() - .get(&method_key(env, user, method_id)) +fn get_method(env: &Env, storage: &Address, user: &Address, method_id: PaymentMethodId) -> Option { + storage_persistent_get(env, storage, StorageKey::PaymentMethodEntry(user.clone(), method_id)) } -fn set_method(env: &Env, user: &Address, method_id: PaymentMethodId, method: &PaymentMethod) { - env.storage() - .persistent() - .set(&method_key(env, user, method_id), method); +fn set_method(env: &Env, storage: &Address, user: &Address, method_id: PaymentMethodId, method: &PaymentMethod) { + storage_persistent_set(env, storage, StorageKey::PaymentMethodEntry(user.clone(), method_id), method.clone()); } -fn remove_method(env: &Env, user: &Address, method_id: PaymentMethodId) { - env.storage() - .persistent() - .remove(&method_key(env, user, method_id)); +fn remove_method(env: &Env, storage: &Address, user: &Address, method_id: PaymentMethodId) { + storage_persistent_remove(env, storage, StorageKey::PaymentMethodEntry(user.clone(), method_id)); } fn sort_by_priority(env: &Env, storage: &Address, user: &Address) -> Vec { - let method_ids = get_user_method_ids(env, user); + let method_ids = get_user_method_ids(env, storage, user); let mut methods: Vec = Vec::new(env); for id in method_ids.iter() { - if let Some(method) = get_method(env, user, id) { + if let Some(method) = get_method(env, storage, user, id) { if method.is_active && method.is_verified { methods.push_back(method); } } } - let _ = storage; let mut i = 0u32; let len = methods.len(); @@ -130,6 +97,7 @@ fn check_expiring_soon(method: &PaymentMethod, env: &Env) -> bool { pub(crate) fn add_payment_method( env: &Env, + storage: &Address, user: &Address, token_type: TokenType, token_address: Address, @@ -138,7 +106,7 @@ pub(crate) fn add_payment_method( priority: PaymentPriority, max_spend_per_interval: i128, ) -> PaymentMethodId { - let count = get_user_count(env, user); + let count = get_user_count(env, storage, user); assert!( count < MAX_PAYMENT_METHODS as u64, "Maximum payment methods reached (10)" @@ -169,12 +137,12 @@ pub(crate) fn add_payment_method( metadata: Vec::new(env), }; - set_method(env, user, new_id, &method); + set_method(env, storage, user, new_id, &method); - let mut user_methods = get_user_method_ids(env, user); + let mut user_methods = get_user_method_ids(env, storage, user); user_methods.push_back(new_id); - set_user_method_ids(env, user, user_methods); - set_user_count(env, user, new_id); + set_user_method_ids(env, storage, user, user_methods); + set_user_count(env, storage, user, new_id); env.events().publish( (String::from_str(env, "payment_method_added"), user.clone()), @@ -189,20 +157,20 @@ pub(crate) fn add_payment_method( new_id } -pub(crate) fn remove_payment_method(env: &Env, user: &Address, method_id: PaymentMethodId) { - let method = get_method(env, user, method_id).expect("Payment method not found"); +pub(crate) fn remove_payment_method(env: &Env, storage: &Address, user: &Address, method_id: PaymentMethodId) { + let method = get_method(env, storage, user, method_id).expect("Payment method not found"); assert!(method.user == *user, "Only owner can remove payment method"); - remove_method(env, user, method_id); + remove_method(env, storage, user, method_id); - let user_methods = get_user_method_ids(env, user); + let user_methods = get_user_method_ids(env, storage, user); let mut updated: Vec = Vec::new(env); for id in user_methods.iter() { if id != method_id { updated.push_back(id); } } - set_user_method_ids(env, user, updated); + set_user_method_ids(env, storage, user, updated); env.events().publish( ( @@ -213,15 +181,15 @@ pub(crate) fn remove_payment_method(env: &Env, user: &Address, method_id: Paymen ); } -pub(crate) fn verify_payment_method(env: &Env, user: &Address, method_id: PaymentMethodId) { - let mut method = get_method(env, user, method_id).expect("Payment method not found"); +pub(crate) fn verify_payment_method(env: &Env, storage: &Address, user: &Address, method_id: PaymentMethodId) { + let mut method = get_method(env, storage, user, method_id).expect("Payment method not found"); assert!(method.user == *user, "Only owner can verify"); let now = env.ledger().timestamp(); method.is_verified = true; method.updated_at = now; - set_method(env, user, method_id, &method); + set_method(env, storage, user, method_id, &method); env.events().publish( ( @@ -234,18 +202,19 @@ pub(crate) fn verify_payment_method(env: &Env, user: &Address, method_id: Paymen pub(crate) fn set_payment_method_priority( env: &Env, + storage: &Address, user: &Address, method_id: PaymentMethodId, priority: PaymentPriority, ) { - let mut method = get_method(env, user, method_id).expect("Payment method not found"); + let mut method = get_method(env, storage, user, method_id).expect("Payment method not found"); assert!(method.user == *user, "Only owner can change priority"); let now = env.ledger().timestamp(); method.priority = priority.clone(); method.updated_at = now; - set_method(env, user, method_id, &method); + set_method(env, storage, user, method_id, &method); env.events().publish( ( @@ -258,18 +227,19 @@ pub(crate) fn set_payment_method_priority( pub(crate) fn set_payment_method_expiry( env: &Env, + storage: &Address, user: &Address, method_id: PaymentMethodId, expires_at: u64, ) { - let mut method = get_method(env, user, method_id).expect("Payment method not found"); + let mut method = get_method(env, storage, user, method_id).expect("Payment method not found"); assert!(method.user == *user, "Only owner can set expiry"); let now = env.ledger().timestamp(); method.expires_at = expires_at; method.updated_at = now; - set_method(env, user, method_id, &method); + set_method(env, storage, user, method_id, &method); env.events().publish( ( @@ -282,13 +252,14 @@ pub(crate) fn set_payment_method_expiry( pub(crate) fn charge_with_fallback( env: &Env, + storage: &Address, user: &Address, merchant: &Address, - token_address: &Address, + _token_address: &Address, amount: i128, subscription_id: u64, ) -> bool { - let sorted = sort_by_priority(env, &user.clone(), user); + let sorted = sort_by_priority(env, storage, user); if sorted.len() == 0 { env.events().publish( @@ -346,10 +317,10 @@ pub(crate) fn charge_with_fallback( token::Client::new(env, &method.token_address).transfer(user, merchant, &amount); - let mut updated = get_method(env, user, method.id).unwrap_or(method.clone()); + let mut updated = get_method(env, storage, user, method.id).unwrap_or(method.clone()); updated.last_used_at = now; updated.updated_at = now; - set_method(env, user, method.id, &updated); + set_method(env, storage, user, method.id, &updated); env.events().publish( ( @@ -381,22 +352,23 @@ pub(crate) fn charge_with_fallback( pub(crate) fn get_payment_method( env: &Env, + storage: &Address, user: &Address, method_id: PaymentMethodId, ) -> PaymentMethod { - get_method(env, user, method_id).expect("Payment method not found") + get_method(env, storage, user, method_id).expect("Payment method not found") } -pub(crate) fn list_payment_methods(env: &Env, user: &Address) -> Vec { - sort_by_priority(env, &user.clone(), user) +pub(crate) fn list_payment_methods(env: &Env, storage: &Address, user: &Address) -> Vec { + sort_by_priority(env, storage, user) } -pub(crate) fn get_expired_methods(env: &Env, user: &Address) -> Vec { - let method_ids = get_user_method_ids(env, user); +pub(crate) fn get_expired_methods(env: &Env, storage: &Address, user: &Address) -> Vec { + let method_ids = get_user_method_ids(env, storage, user); let mut expired: Vec = Vec::new(env); for id in method_ids.iter() { - if let Some(method) = get_method(env, user, id) { + if let Some(method) = get_method(env, storage, user, id) { if check_expired(&method, env) { expired.push_back(id); } @@ -406,12 +378,12 @@ pub(crate) fn get_expired_methods(env: &Env, user: &Address) -> Vec Vec { - let method_ids = get_user_method_ids(env, user); +pub(crate) fn get_expiring_soon_methods(env: &Env, storage: &Address, user: &Address) -> Vec { + let method_ids = get_user_method_ids(env, storage, user); let mut expiring: Vec = Vec::new(env); for id in method_ids.iter() { - if let Some(method) = get_method(env, user, id) { + if let Some(method) = get_method(env, storage, user, id) { if check_expiring_soon(&method, env) { expiring.push_back(id); } @@ -421,31 +393,17 @@ pub(crate) fn get_expiring_soon_methods(env: &Env, user: &Address) -> Vec u32 { - let expired_ids = get_expired_methods(env, user); +pub(crate) fn deactivate_expired_methods(env: &Env, storage: &Address, user: &Address) -> u32 { + let expired_ids = get_expired_methods(env, storage, user); let count = expired_ids.len() as u32; let now = env.ledger().timestamp(); for id in expired_ids.iter() { - if let Some(mut method) = get_method(env, user, id) { + if let Some(mut method) = get_method(env, storage, user, id) { method.is_active = false; method.updated_at = now; - let mut meta = match method.metadata.is_empty() { - true => Vec::new(env), - false => method.metadata.clone(), - }; - meta.push_back(( - String::from_str(env, "deactivated_reason"), - String::from_str(env, "expired"), - )); - meta.push_back(( - String::from_str(env, "deactivated_at"), - String::from_str(env, &now.to_string()), - )); - method.metadata = meta; - - set_method(env, user, id, &method); + set_method(env, storage, user, id, &method); } } diff --git a/contracts/subscription/src/quota.rs b/contracts/subscription/src/quota.rs index 784d80b3..39336e6c 100644 --- a/contracts/subscription/src/quota.rs +++ b/contracts/subscription/src/quota.rs @@ -1,11 +1,11 @@ use crate::{storage_persistent_get, storage_persistent_set}; use soroban_sdk::{Address, Env, Vec}; -use subtrackr_types::{Quota, StorageKeyExt}; +use subtrackr_types::{Quota, StorageKey}; pub fn set_plan_quotas(env: &Env, storage: &Address, plan_id: u64, quotas: Vec) { - storage_persistent_set(env, storage, StorageKeyExt::PlanQuotas(plan_id), quotas); + storage_persistent_set(env, storage, StorageKey::PlanQuotas(plan_id), quotas); } pub fn get_plan_quotas(env: &Env, storage: &Address, plan_id: u64) -> Vec { - storage_persistent_get(env, storage, StorageKeyExt::PlanQuotas(plan_id)).unwrap_or(Vec::new(env)) + storage_persistent_get(env, storage, StorageKey::PlanQuotas(plan_id)).unwrap_or(Vec::new(env)) } diff --git a/contracts/subscription/src/revenue.rs b/contracts/subscription/src/revenue.rs index 2fd7a32a..2688986f 100644 --- a/contracts/subscription/src/revenue.rs +++ b/contracts/subscription/src/revenue.rs @@ -8,7 +8,7 @@ /// All storage is delegated to the shared storage contract via the /// `storage_persistent_*` helpers defined in the parent module. use soroban_sdk::{contracttype, Address, Env, Vec}; -use subtrackr_types::StorageKeyExt; +use subtrackr_types::StorageKey; use crate::{storage_persistent_get, storage_persistent_set}; @@ -153,7 +153,7 @@ pub fn set_recognition_rule(env: &Env, storage: &Address, rule: RevenueRecogniti storage_persistent_set( env, storage, - StorageKeyExt::RevenueRecognitionRule(rule.plan_id), + StorageKey::RevenueRecognitionRule(rule.plan_id), rule, ); } @@ -163,7 +163,7 @@ pub fn get_recognition_rule( storage: &Address, plan_id: u64, ) -> Option { - storage_persistent_get(env, storage, StorageKeyExt::RevenueRecognitionRule(plan_id)) + storage_persistent_get(env, storage, StorageKey::RevenueRecognitionRule(plan_id)) } pub fn get_revenue_schedule( @@ -171,14 +171,14 @@ pub fn get_revenue_schedule( storage: &Address, subscription_id: u64, ) -> Option { - storage_persistent_get(env, storage, StorageKeyExt::RevenueSchedule(subscription_id)) + storage_persistent_get(env, storage, StorageKey::RevenueSchedule(subscription_id)) } pub fn get_deferred_revenue(env: &Env, storage: &Address, merchant: &Address) -> i128 { storage_persistent_get( env, storage, - StorageKeyExt::RevenueDeferredBalance(merchant.clone()), + StorageKey::RevenueDeferredBalance(merchant.clone()), ) .unwrap_or(0i128) } @@ -233,7 +233,7 @@ pub fn generate_revenue_schedule( storage_persistent_set( env, storage, - StorageKeyExt::RevenueSchedule(subscription_id), + StorageKey::RevenueSchedule(subscription_id), schedule.clone(), ); schedule @@ -279,26 +279,26 @@ pub fn update_merchant_revenue_balances( let prev_rec: i128 = storage_persistent_get( env, storage, - StorageKeyExt::RevenueRecognisedBalance(merchant.clone()), + StorageKey::RevenueRecognisedBalance(merchant.clone()), ) .unwrap_or(0i128); let prev_def: i128 = storage_persistent_get( env, storage, - StorageKeyExt::RevenueDeferredBalance(merchant.clone()), + StorageKey::RevenueDeferredBalance(merchant.clone()), ) .unwrap_or(0i128); storage_persistent_set( env, storage, - StorageKeyExt::RevenueRecognisedBalance(merchant.clone()), + StorageKey::RevenueRecognisedBalance(merchant.clone()), prev_rec + recognised_delta, ); storage_persistent_set( env, storage, - StorageKeyExt::RevenueDeferredBalance(merchant.clone()), + StorageKey::RevenueDeferredBalance(merchant.clone()), prev_def + deferred_delta, ); } @@ -313,7 +313,7 @@ pub fn track_merchant_subscription( let mut ids: Vec = storage_persistent_get( env, storage, - StorageKeyExt::RevenueMerchantSubscriptions(merchant.clone()), + StorageKey::RevenueMerchantSubscriptions(merchant.clone()), ) .unwrap_or(Vec::new(env)); for existing in ids.iter() { @@ -325,7 +325,7 @@ pub fn track_merchant_subscription( storage_persistent_set( env, storage, - StorageKeyExt::RevenueMerchantSubscriptions(merchant.clone()), + StorageKey::RevenueMerchantSubscriptions(merchant.clone()), ids, ); } @@ -345,7 +345,7 @@ pub fn get_revenue_analytics_by_period( let sub_ids: Vec = storage_persistent_get( env, storage, - StorageKeyExt::RevenueMerchantSubscriptions(merchant.clone()), + StorageKey::RevenueMerchantSubscriptions(merchant.clone()), ) .unwrap_or(Vec::new(env)); @@ -363,7 +363,7 @@ pub fn get_revenue_analytics_by_period( for sub_id in sub_ids.iter() { let maybe: Option = - storage_persistent_get(env, storage, StorageKeyExt::RevenueSchedule(sub_id)); + storage_persistent_get(env, storage, StorageKey::RevenueSchedule(sub_id)); if let Some(schedule) = maybe { let mut contributed = false; for entry in schedule.entries.iter() { diff --git a/contracts/subscription/src/subtrackr_subscription.rs b/contracts/subscription/src/subtrackr_subscription.rs index e69de29b..c17357bf 100644 --- a/contracts/subscription/src/subtrackr_subscription.rs +++ b/contracts/subscription/src/subtrackr_subscription.rs @@ -0,0 +1,1024 @@ +use soroban_sdk::{Address, BytesN, Env, IntoVal, String, Vec}; +use subtrackr_types::{ + ChargeCommitment, Interval, MevProtectionConfig, Plan, StorageKey, Subscription, + SubscriptionStatus, +}; + +use crate::{ + build_charge_commitment, charge_subscription_guarded, check_and_resume_internal, + enforce_rate_limit, get_admin, get_mev_config, get_user_plan_index, + remove_user_plan_index, set_user_plan_index, storage_instance_get, storage_instance_remove, + storage_instance_set, storage_persistent_get, storage_persistent_remove, storage_persistent_set, + validate_mev_config, MAX_PAUSE_DURATION, STORAGE_VERSION, +}; +use crate::quota; +use crate::revenue; +use crate::usage; + +#[soroban_sdk::contract] +pub struct SubTrackrSubscription; + +#[soroban_sdk::contractimpl] +impl SubTrackrSubscription { + // ── Upgrade interface ── + + pub fn get_version(_env: Env, proxy: Address, _storage: Address) -> u32 { + proxy.require_auth(); + STORAGE_VERSION + } + + pub fn validate_upgrade(env: Env, proxy: Address, storage: Address, from_version: u32) { + proxy.require_auth(); + assert!(from_version > 0, "Invalid version"); + assert!( + from_version <= STORAGE_VERSION, + "Cannot upgrade from future version" + ); + + // Ensure core keys exist before allowing upgrade/migration. + let _admin: Address = get_admin(&env, &storage); + let _plan_count: u64 = + storage_instance_get(&env, &storage, StorageKey::PlanCount).unwrap_or(0); + let _sub_count: u64 = + storage_instance_get(&env, &storage, StorageKey::SubscriptionCount).unwrap_or(0); + } + + /// Migrate storage from `from_version` to this implementation's `STORAGE_VERSION`. + /// + /// For v1 -> v2: build `UserPlanIndex` for all active/non-cancelled subscriptions. + pub fn migrate(env: Env, proxy: Address, storage: Address, from_version: u32) { + proxy.require_auth(); + if from_version == STORAGE_VERSION { + return; + } + assert!(from_version < STORAGE_VERSION, "Unsupported migration path"); + + if from_version == 1 { + let sub_count: u64 = + storage_instance_get(&env, &storage, StorageKey::SubscriptionCount).unwrap_or(0); + let mut i: u64 = 1; + while i <= sub_count { + let sub_opt: Option = + storage_persistent_get(&env, &storage, StorageKey::Subscription(i)); + if let Some(sub) = sub_opt { + if sub.status != SubscriptionStatus::Cancelled { + set_user_plan_index(&env, &storage, &sub.subscriber, sub.plan_id, sub.id); + } + } + i += 1; + } + return; + } + + panic!("Unsupported migration path"); + } + + // ── Initialization ── + + pub fn initialize(env: Env, proxy: Address, storage: Address, admin: Address) { + proxy.require_auth(); + admin.require_auth(); + + storage_instance_set(&env, &storage, StorageKey::Admin, admin); + storage_instance_set(&env, &storage, StorageKey::PlanCount, 0u64); + storage_instance_set(&env, &storage, StorageKey::SubscriptionCount, 0u64); + storage_instance_remove(&env, &storage, StorageKey::InvoiceContract); + } + + pub fn set_invoice_contract(env: Env, proxy: Address, storage: Address, invoice: Address) { + proxy.require_auth(); + let admin = get_admin(&env, &storage); + admin.require_auth(); + storage_instance_set(&env, &storage, StorageKey::InvoiceContract, invoice); + } + + pub fn clear_invoice_contract(env: Env, proxy: Address, storage: Address) { + proxy.require_auth(); + let admin = get_admin(&env, &storage); + admin.require_auth(); + storage_instance_remove(&env, &storage, StorageKey::InvoiceContract); + } + + // ── Rate Limiting Admin ── + + pub fn set_rate_limit( + env: Env, + proxy: Address, + storage: Address, + function: String, + min_interval_secs: u64, + ) { + proxy.require_auth(); + let admin = get_admin(&env, &storage); + admin.require_auth(); + storage_instance_set( + &env, + &storage, + StorageKey::RateLimit(function), + min_interval_secs, + ); + } + + pub fn remove_rate_limit(env: Env, proxy: Address, storage: Address, function: String) { + proxy.require_auth(); + let admin = get_admin(&env, &storage); + admin.require_auth(); + storage_instance_remove(&env, &storage, StorageKey::RateLimit(function)); + } + + pub fn configure_mev_protection( + env: Env, + proxy: Address, + storage: Address, + admin: Address, + config: MevProtectionConfig, + ) { + proxy.require_auth(); + assert!(admin == get_admin(&env, &storage), "Admin mismatch"); + admin.require_auth(); + validate_mev_config(&config); + storage_instance_set( + &env, + &storage, + StorageKey::MevProtectionConfig, + config.clone(), + ); + env.events().publish( + (String::from_str(&env, "mev_configured"), admin), + ( + config.large_charge_threshold, + config.max_fee_bps, + config.private_mempool_required, + config.gas_price_alert_threshold, + ), + ); + } + + pub fn commit_charge( + env: Env, + proxy: Address, + storage: Address, + subscription_id: u64, + commitment: BytesN<32>, + ) { + proxy.require_auth(); + let sub: Subscription = + storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) + .expect("Subscription not found"); + sub.subscriber.require_auth(); + + let now = env.ledger().timestamp(); + if let Some(existing) = storage_persistent_get::( + &env, + &storage, + StorageKey::ChargeCommitment(subscription_id), + ) { + assert!( + now > existing.expires_at, + "Pending charge commitment exists" + ); + } + + let config = get_mev_config(&env, &storage); + let pending = ChargeCommitment { + subscription_id, + subscriber: sub.subscriber.clone(), + commitment: commitment.clone(), + committed_at: now, + min_reveal_at: now + config.reveal_delay_secs, + expires_at: now + config.commit_ttl_secs, + }; + storage_persistent_set( + &env, + &storage, + StorageKey::ChargeCommitment(subscription_id), + pending.clone(), + ); + env.events().publish( + (String::from_str(&env, "charge_committed"), subscription_id), + ( + pending.subscriber, + pending.min_reveal_at, + pending.expires_at, + ), + ); + } + + pub fn hash_charge_commitment( + env: Env, + _proxy: Address, + _storage: Address, + subscription_id: u64, + max_charge_amount: i128, + salt: BytesN<32>, + ) -> BytesN<32> { + build_charge_commitment(&env, subscription_id, max_charge_amount, &salt) + } + + pub fn reveal_charge( + env: Env, + proxy: Address, + storage: Address, + subscription_id: u64, + salt: BytesN<32>, + max_charge_amount: i128, + observed_gas_price: u64, + private_mempool: bool, + ) { + proxy.require_auth(); + let pending: ChargeCommitment = storage_persistent_get( + &env, + &storage, + StorageKey::ChargeCommitment(subscription_id), + ) + .expect("Charge commitment not found"); + + let now = env.ledger().timestamp(); + assert!(now >= pending.min_reveal_at, "Reveal delay not met"); + assert!(now <= pending.expires_at, "Charge commitment expired"); + let revealed_commitment = + build_charge_commitment(&env, subscription_id, max_charge_amount, &salt); + assert!( + pending.commitment == revealed_commitment, + "Commitment mismatch" + ); + pending.subscriber.require_auth(); + + storage_persistent_remove( + &env, + &storage, + StorageKey::ChargeCommitment(subscription_id), + ); + env.events().publish( + (String::from_str(&env, "charge_revealed"), subscription_id), + (max_charge_amount, observed_gas_price, private_mempool, now), + ); + + charge_subscription_guarded( + &env, + &storage, + subscription_id, + max_charge_amount, + observed_gas_price, + private_mempool, + true, + ); + } + + // ── Plan Management ── + + pub fn create_plan( + env: Env, + proxy: Address, + storage: Address, + merchant: Address, + name: String, + price: i128, + token: Address, + interval: Interval, + ) -> u64 { + proxy.require_auth(); + if merchant != get_admin(&env, &storage) { + enforce_rate_limit(&env, &storage, &merchant, "create_plan"); + } + merchant.require_auth(); + assert!(price > 0, "Price must be positive"); + + let mut count: u64 = + storage_instance_get(&env, &storage, StorageKey::PlanCount).unwrap_or(0); + count += 1; + + let plan = Plan { + id: count, + merchant: merchant.clone(), + name, + price, + token, + interval, + active: true, + subscriber_count: 0, + created_at: env.ledger().timestamp(), + }; + + storage_persistent_set(&env, &storage, StorageKey::Plan(count), plan.clone()); + storage_instance_set(&env, &storage, StorageKey::PlanCount, count); + + let mut merchant_plans: Vec = + storage_persistent_get(&env, &storage, StorageKey::MerchantPlans(merchant.clone())) + .unwrap_or(Vec::new(&env)); + merchant_plans.push_back(count); + storage_persistent_set( + &env, + &storage, + StorageKey::MerchantPlans(merchant), + merchant_plans, + ); + + count + } + + pub fn deactivate_plan( + env: Env, + proxy: Address, + storage: Address, + merchant: Address, + plan_id: u64, + ) { + proxy.require_auth(); + if merchant != get_admin(&env, &storage) { + enforce_rate_limit(&env, &storage, &merchant, "deactivate_plan"); + } + merchant.require_auth(); + + let mut plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(plan_id)) + .expect("Plan not found"); + + assert!(plan.merchant == merchant, "Only plan owner can deactivate"); + plan.active = false; + + storage_persistent_set(&env, &storage, StorageKey::Plan(plan_id), plan); + } + + // ── Subscription Management ── + + pub fn subscribe( + env: Env, + proxy: Address, + storage: Address, + subscriber: Address, + plan_id: u64, + ) -> u64 { + proxy.require_auth(); + if subscriber != get_admin(&env, &storage) { + enforce_rate_limit(&env, &storage, &subscriber, "subscribe"); + } + subscriber.require_auth(); + + let mut plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(plan_id)) + .expect("Plan not found"); + assert!(plan.active, "Plan is not active"); + assert!( + plan.merchant != subscriber, + "Merchant cannot self-subscribe" + ); + + if let Some(existing_id) = get_user_plan_index(&env, &storage, &subscriber, plan_id) { + let existing_sub: Subscription = + storage_persistent_get(&env, &storage, StorageKey::Subscription(existing_id)) + .expect("Subscription not found"); + if existing_sub.status != SubscriptionStatus::Cancelled { + panic!("Already subscribed to this plan"); + } + } + + let mut sub_count: u64 = + storage_instance_get(&env, &storage, StorageKey::SubscriptionCount).unwrap_or(0); + sub_count += 1; + + let now = env.ledger().timestamp(); + + let subscription = Subscription { + id: sub_count, + plan_id, + subscriber: subscriber.clone(), + status: SubscriptionStatus::Active, + started_at: now, + last_charged_at: now, + next_charge_at: now + plan.interval.seconds(), + total_paid: 0, + total_gas_spent: 0, + charge_count: 0, + paused_at: 0, + pause_duration: 0, + refund_requested_amount: 0, + }; + + storage_persistent_set( + &env, + &storage, + StorageKey::Subscription(sub_count), + subscription, + ); + storage_instance_set(&env, &storage, StorageKey::SubscriptionCount, sub_count); + + let mut user_subs: Vec = storage_persistent_get( + &env, + &storage, + StorageKey::UserSubscriptions(subscriber.clone()), + ) + .unwrap_or(Vec::new(&env)); + user_subs.push_back(sub_count); + storage_persistent_set( + &env, + &storage, + StorageKey::UserSubscriptions(subscriber.clone()), + user_subs, + ); + + // Index for quick duplicate checks + set_user_plan_index(&env, &storage, &subscriber, plan_id, sub_count); + + plan.subscriber_count += 1; + storage_persistent_set(&env, &storage, StorageKey::Plan(plan_id), plan); + + sub_count + } + + pub fn cancel_subscription( + env: Env, + proxy: Address, + storage: Address, + subscriber: Address, + subscription_id: u64, + ) { + proxy.require_auth(); + if subscriber != get_admin(&env, &storage) { + enforce_rate_limit(&env, &storage, &subscriber, "cancel_subscription"); + } + subscriber.require_auth(); + + let mut sub: Subscription = + storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) + .expect("Subscription not found"); + + assert!(sub.subscriber == subscriber, "Only subscriber can cancel"); + assert!( + sub.status == SubscriptionStatus::Active || sub.status == SubscriptionStatus::Paused, + "Subscription not active" + ); + + sub.status = SubscriptionStatus::Cancelled; + storage_persistent_set( + &env, + &storage, + StorageKey::Subscription(subscription_id), + sub.clone(), + ); + + // Remove index + remove_user_plan_index(&env, &storage, &subscriber, sub.plan_id); + + let mut plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(sub.plan_id)) + .expect("Plan not found"); + if plan.subscriber_count > 0 { + plan.subscriber_count -= 1; + } + storage_persistent_set(&env, &storage, StorageKey::Plan(sub.plan_id), plan); + } + + pub fn pause_subscription( + env: Env, + proxy: Address, + storage: Address, + subscriber: Address, + subscription_id: u64, + ) { + proxy.require_auth(); + if subscriber != get_admin(&env, &storage) { + enforce_rate_limit(&env, &storage, &subscriber, "pause_subscription"); + } + Self::pause_by_subscriber( + env, + proxy, + storage, + subscriber, + subscription_id, + MAX_PAUSE_DURATION, + ); + } + + pub fn pause_by_subscriber( + env: Env, + proxy: Address, + storage: Address, + subscriber: Address, + subscription_id: u64, + duration: u64, + ) { + proxy.require_auth(); + if subscriber != get_admin(&env, &storage) { + enforce_rate_limit(&env, &storage, &subscriber, "pause_by_subscriber"); + } + subscriber.require_auth(); + + let mut sub: Subscription = + storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) + .expect("Subscription not found"); + + assert!(sub.subscriber == subscriber, "Only subscriber can pause"); + assert!( + sub.status == SubscriptionStatus::Active, + "Only active subscriptions can be paused" + ); + assert!( + duration <= MAX_PAUSE_DURATION, + "Pause duration exceeds limit" + ); + + sub.status = SubscriptionStatus::Paused; + sub.paused_at = env.ledger().timestamp(); + sub.pause_duration = duration; + + storage_persistent_set( + &env, + &storage, + StorageKey::Subscription(subscription_id), + sub.clone(), + ); + + env.events().publish( + (String::from_str(&env, "subscription_paused"), subscriber), + (subscription_id, sub.paused_at, duration), + ); + } + + pub fn resume_subscription( + env: Env, + proxy: Address, + storage: Address, + subscriber: Address, + subscription_id: u64, + ) { + proxy.require_auth(); + if subscriber != get_admin(&env, &storage) { + enforce_rate_limit(&env, &storage, &subscriber, "resume_subscription"); + } + subscriber.require_auth(); + + let mut sub: Subscription = + storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) + .expect("Subscription not found"); + + assert!(sub.subscriber == subscriber, "Only subscriber can resume"); + assert!( + sub.status == SubscriptionStatus::Paused || check_and_resume_internal(&env, &mut sub), + "Only paused subscriptions can be resumed" + ); + + let now = env.ledger().timestamp(); + let plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(sub.plan_id)) + .expect("Plan not found"); + + sub.status = SubscriptionStatus::Active; + sub.next_charge_at = now + plan.interval.seconds(); + sub.paused_at = 0; + sub.pause_duration = 0; + + storage_persistent_set( + &env, + &storage, + StorageKey::Subscription(subscription_id), + sub, + ); + + env.events().publish( + (String::from_str(&env, "subscription_resumed"), subscriber), + subscription_id, + ); + } + + // ── Payment Processing ── + + pub fn charge_subscription(env: Env, proxy: Address, storage: Address, subscription_id: u64) { + proxy.require_auth(); + charge_subscription_guarded(&env, &storage, subscription_id, i128::MAX, 0, false, false); + } + + pub fn request_refund( + env: Env, + proxy: Address, + storage: Address, + subscription_id: u64, + amount: i128, + ) { + proxy.require_auth(); + let mut sub: Subscription = + storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) + .expect("Subscription not found"); + + if sub.subscriber != get_admin(&env, &storage) { + enforce_rate_limit(&env, &storage, &sub.subscriber, "request_refund"); + } + + sub.subscriber.require_auth(); + + assert!(amount > 0, "Refund amount must be positive"); + assert!( + amount <= sub.total_paid, + "Refund amount cannot exceed total paid" + ); + + sub.refund_requested_amount = amount; + storage_persistent_set( + &env, + &storage, + StorageKey::Subscription(subscription_id), + sub.clone(), + ); + + env.events().publish( + (String::from_str(&env, "refund_requested"), subscription_id), + (sub.subscriber.clone(), amount), + ); + } + + pub fn approve_refund(env: Env, proxy: Address, storage: Address, subscription_id: u64) { + proxy.require_auth(); + let mut sub: Subscription = + storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) + .expect("Subscription not found"); + + let admin = get_admin(&env, &storage); + admin.require_auth(); + + let amount = sub.refund_requested_amount; + assert!(amount > 0, "No pending refund request"); + + let _plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(sub.plan_id)) + .expect("Plan not found"); + + sub.total_paid -= amount; + sub.refund_requested_amount = 0; + + storage_persistent_set( + &env, + &storage, + StorageKey::Subscription(subscription_id), + sub.clone(), + ); + + env.events().publish( + (String::from_str(&env, "refund_approved"), subscription_id), + (sub.subscriber.clone(), amount), + ); + } + + pub fn reject_refund(env: Env, proxy: Address, storage: Address, subscription_id: u64) { + proxy.require_auth(); + let mut sub: Subscription = + storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) + .expect("Subscription not found"); + + let admin = get_admin(&env, &storage); + admin.require_auth(); + + assert!(sub.refund_requested_amount > 0, "No pending refund request"); + sub.refund_requested_amount = 0; + + storage_persistent_set( + &env, + &storage, + StorageKey::Subscription(subscription_id), + sub.clone(), + ); + + env.events().publish( + (String::from_str(&env, "refund_rejected"), subscription_id), + sub.subscriber.clone(), + ); + } + + // ── Subscription Transfer ── + + pub fn request_transfer( + env: Env, + proxy: Address, + storage: Address, + subscription_id: u64, + recipient: Address, + ) { + proxy.require_auth(); + let sub: Subscription = + storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) + .expect("Subscription not found"); + + if sub.subscriber != get_admin(&env, &storage) { + enforce_rate_limit(&env, &storage, &sub.subscriber, "request_transfer"); + } + + sub.subscriber.require_auth(); + assert!( + sub.status != SubscriptionStatus::Cancelled, + "Subscription is cancelled" + ); + assert!(sub.subscriber != recipient, "Cannot transfer to self"); + + storage_instance_set( + &env, + &storage, + StorageKey::PendingTransfer(subscription_id), + recipient.clone(), + ); + + env.events().publish( + ( + String::from_str(&env, "transfer_requested"), + subscription_id, + ), + (sub.subscriber.clone(), recipient), + ); + } + + pub fn accept_transfer( + env: Env, + proxy: Address, + storage: Address, + subscription_id: u64, + recipient: Address, + ) { + proxy.require_auth(); + if recipient != get_admin(&env, &storage) { + enforce_rate_limit(&env, &storage, &recipient, "accept_transfer"); + } + recipient.require_auth(); + + let mut sub: Subscription = + storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) + .expect("Subscription not found"); + + let pending_recipient: Address = + storage_instance_get(&env, &storage, StorageKey::PendingTransfer(subscription_id)) + .expect("No pending transfer for this subscription"); + assert!( + pending_recipient == recipient, + "Transfer recipient mismatch" + ); + + let old_user_subs: Vec = storage_persistent_get( + &env, + &storage, + StorageKey::UserSubscriptions(sub.subscriber.clone()), + ) + .unwrap_or(Vec::new(&env)); + let mut new_list: Vec = Vec::new(&env); + for id in old_user_subs.iter() { + if id != subscription_id { + new_list.push_back(id); + } + } + storage_persistent_set( + &env, + &storage, + StorageKey::UserSubscriptions(sub.subscriber.clone()), + new_list, + ); + + let mut rec_user_subs: Vec = storage_persistent_get( + &env, + &storage, + StorageKey::UserSubscriptions(recipient.clone()), + ) + .unwrap_or(Vec::new(&env)); + rec_user_subs.push_back(subscription_id); + storage_persistent_set( + &env, + &storage, + StorageKey::UserSubscriptions(recipient.clone()), + rec_user_subs, + ); + + // Update index mapping + remove_user_plan_index(&env, &storage, &sub.subscriber, sub.plan_id); + set_user_plan_index(&env, &storage, &recipient, sub.plan_id, sub.id); + + let old = sub.subscriber.clone(); + sub.subscriber = recipient.clone(); + storage_persistent_set( + &env, + &storage, + StorageKey::Subscription(subscription_id), + sub, + ); + + storage_instance_remove(&env, &storage, StorageKey::PendingTransfer(subscription_id)); + + env.events().publish( + (String::from_str(&env, "transfer_accepted"), subscription_id), + (old, recipient), + ); + } + + // ── Queries ── + + pub fn get_plan(env: Env, proxy: Address, storage: Address, plan_id: u64) -> Plan { + proxy.require_auth(); + storage_persistent_get(&env, &storage, StorageKey::Plan(plan_id)).expect("Plan not found") + } + + pub fn get_subscription( + env: Env, + proxy: Address, + storage: Address, + subscription_id: u64, + ) -> Subscription { + proxy.require_auth(); + let mut sub: Subscription = + storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) + .expect("Subscription not found"); + + check_and_resume_internal(&env, &mut sub); + sub + } + + pub fn get_user_subscriptions( + env: Env, + proxy: Address, + storage: Address, + subscriber: Address, + ) -> Vec { + proxy.require_auth(); + storage_persistent_get(&env, &storage, StorageKey::UserSubscriptions(subscriber)) + .unwrap_or(Vec::new(&env)) + } + + pub fn get_merchant_plans( + env: Env, + proxy: Address, + storage: Address, + merchant: Address, + ) -> Vec { + proxy.require_auth(); + storage_persistent_get(&env, &storage, StorageKey::MerchantPlans(merchant)) + .unwrap_or(Vec::new(&env)) + } + + pub fn get_plan_count(env: Env, proxy: Address, storage: Address) -> u64 { + proxy.require_auth(); + storage_instance_get(&env, &storage, StorageKey::PlanCount).unwrap_or(0) + } + + pub fn get_subscription_count(env: Env, proxy: Address, storage: Address) -> u64 { + proxy.require_auth(); + storage_instance_get(&env, &storage, StorageKey::SubscriptionCount).unwrap_or(0) + } + + pub fn get_mev_protection_config( + env: Env, + proxy: Address, + storage: Address, + ) -> MevProtectionConfig { + proxy.require_auth(); + get_mev_config(&env, &storage) + } + + pub fn get_charge_commitment( + env: Env, + proxy: Address, + storage: Address, + subscription_id: u64, + ) -> Option { + proxy.require_auth(); + storage_persistent_get( + &env, + &storage, + StorageKey::ChargeCommitment(subscription_id), + ) + } + + pub fn get_mev_alert_count(env: Env, proxy: Address, storage: Address) -> u64 { + proxy.require_auth(); + storage_instance_get(&env, &storage, StorageKey::MevAlertCount).unwrap_or(0) + } + + // ── Revenue Recognition API ── + + /// Set a revenue recognition rule for a plan (merchant only). + pub fn set_revenue_rule( + env: Env, + proxy: Address, + storage: Address, + merchant: Address, + plan_id: u64, + method: revenue::RecognitionMethod, + recognition_period: u64, + ) { + proxy.require_auth(); + merchant.require_auth(); + let plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(plan_id)) + .expect("Plan not found"); + assert!( + plan.merchant == merchant, + "Only plan owner can set revenue rule" + ); + revenue::set_recognition_rule( + &env, + &storage, + revenue::RevenueRecognitionRule { + plan_id, + method, + recognition_period, + }, + ); + } + + /// Compute a recognition snapshot for a subscription as of the current ledger time. + pub fn recognize_revenue( + env: Env, + proxy: Address, + storage: Address, + subscription_id: u64, + ) -> revenue::Recognition { + proxy.require_auth(); + let sub: Subscription = + storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) + .expect("Subscription not found"); + let plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(sub.plan_id)) + .expect("Plan not found"); + let now = env.ledger().timestamp(); + revenue::recognize_revenue(&env, &storage, subscription_id, plan.merchant, now) + } + + /// Return the cumulative deferred revenue balance for a merchant. + pub fn get_deferred_revenue( + env: Env, + proxy: Address, + storage: Address, + merchant_id: Address, + ) -> i128 { + proxy.require_auth(); + revenue::get_deferred_revenue(&env, &storage, &merchant_id) + } + + /// Return the revenue schedule for a subscription (None if not yet generated). + pub fn get_revenue_schedule( + env: Env, + proxy: Address, + storage: Address, + subscription_id: u64, + ) -> Option { + proxy.require_auth(); + revenue::get_revenue_schedule(&env, &storage, subscription_id) + } + + // ── Quota & Usage API ── + + pub fn set_plan_quotas( + env: Env, + proxy: Address, + storage: Address, + merchant: Address, + plan_id: u64, + quotas: Vec, + ) { + proxy.require_auth(); + merchant.require_auth(); + let plan: subtrackr_types::Plan = + storage_persistent_get(&env, &storage, StorageKey::Plan(plan_id)) + .expect("Plan not found"); + assert!(plan.merchant == merchant, "Only plan owner can set quotas"); + quota::set_plan_quotas(&env, &storage, plan_id, quotas); + } + + pub fn get_plan_quotas( + env: Env, + proxy: Address, + storage: Address, + plan_id: u64, + ) -> Vec { + proxy.require_auth(); + quota::get_plan_quotas(&env, &storage, plan_id) + } + + pub fn record_usage( + env: Env, + proxy: Address, + storage: Address, + subscription_id: u64, + metric: subtrackr_types::QuotaMetric, + amount: u64, + ) -> subtrackr_types::UsageRecord { + proxy.require_auth(); + let sub: subtrackr_types::Subscription = + storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) + .expect("Subscription not found"); + + let _admin = get_admin(&env, &storage); + + usage::record_usage(&env, &storage, subscription_id, sub.plan_id, metric, amount) + } + + pub fn get_usage_record( + env: Env, + proxy: Address, + storage: Address, + subscription_id: u64, + metric: subtrackr_types::QuotaMetric, + ) -> subtrackr_types::UsageRecord { + proxy.require_auth(); + usage::get_usage_record(&env, &storage, subscription_id, metric) + } + + pub fn check_quota( + env: Env, + proxy: Address, + storage: Address, + subscription_id: u64, + metric: subtrackr_types::QuotaMetric, + ) -> subtrackr_types::QuotaStatus { + proxy.require_auth(); + let sub: subtrackr_types::Subscription = + storage_persistent_get(&env, &storage, StorageKey::Subscription(subscription_id)) + .expect("Subscription not found"); + usage::check_quota(&env, &storage, subscription_id, sub.plan_id, metric) + } +} diff --git a/contracts/subscription/src/webhook.rs b/contracts/subscription/src/webhook.rs index a7d65b8c..b139d500 100644 --- a/contracts/subscription/src/webhook.rs +++ b/contracts/subscription/src/webhook.rs @@ -1,7 +1,7 @@ use soroban_sdk::{Address, Env, String, Vec}; -use crate::SubTrackrSubscriptionClient; +use crate::{SubTrackrSubscription, SubTrackrSubscriptionClient}; use subtrackr_types::{ - StorageKeyExt, Subscription, SubscriptionStatus, WebhookConfig, WebhookDelivery, + StorageKey, Subscription, SubscriptionStatus, WebhookConfig, WebhookDelivery, WebhookDeliveryStatus, WebhookEventType, WebhookRetryPolicy, }; @@ -11,16 +11,16 @@ use crate::{ }; fn webhook_ids_for_merchant(env: &Env, storage: &Address, merchant: &Address) -> Vec { - storage_persistent_get(env, storage, StorageKeyExt::MerchantWebhooks(merchant.clone())) + storage_persistent_get(env, storage, StorageKey::MerchantWebhooks(merchant.clone())) .unwrap_or(Vec::new(env)) } fn set_webhook_ids_for_merchant(env: &Env, storage: &Address, merchant: &Address, ids: Vec) { - storage_persistent_set(env, storage, StorageKeyExt::MerchantWebhooks(merchant.clone()), ids); + storage_persistent_set(env, storage, StorageKey::MerchantWebhooks(merchant.clone()), ids); } fn deliveries_for_webhook(env: &Env, storage: &Address, webhook_id: u64) -> Vec { - storage_persistent_get(env, storage, StorageKeyExt::WebhookDeliveriesByWebhook(webhook_id)) + storage_persistent_get(env, storage, StorageKey::WebhookDeliveriesByWebhook(webhook_id)) .unwrap_or(Vec::new(env)) } @@ -28,7 +28,7 @@ fn set_deliveries_for_webhook(env: &Env, storage: &Address, webhook_id: u64, ids storage_persistent_set( env, storage, - StorageKeyExt::WebhookDeliveriesByWebhook(webhook_id), + StorageKey::WebhookDeliveriesByWebhook(webhook_id), ids, ); } @@ -46,17 +46,17 @@ fn webhook_supports_event(config: &WebhookConfig, event_type: &WebhookEventType) } fn next_webhook_id(env: &Env, storage: &Address) -> u64 { - let mut count: u64 = storage_instance_get(env, storage, StorageKeyExt::WebhookCount).unwrap_or(0); + let mut count: u64 = storage_instance_get(env, storage, StorageKey::WebhookCount).unwrap_or(0); count += 1; - storage_instance_set(env, storage, StorageKeyExt::WebhookCount, count); + storage_instance_set(env, storage, StorageKey::WebhookCount, count); count } fn next_delivery_id(env: &Env, storage: &Address) -> u64 { let mut count: u64 = - storage_instance_get(env, storage, StorageKeyExt::WebhookDeliveryCount).unwrap_or(0); + storage_instance_get(env, storage, StorageKey::WebhookDeliveryCount).unwrap_or(0); count += 1; - storage_instance_set(env, storage, StorageKeyExt::WebhookDeliveryCount, count); + storage_instance_set(env, storage, StorageKey::WebhookDeliveryCount, count); count } @@ -90,7 +90,7 @@ pub(crate) fn emit_subscription_event( let config_opt: Option = storage_persistent_get( env, storage, - StorageKeyExt::Webhook(webhook_id), + StorageKey::Webhook(webhook_id), ); if let Some(config) = config_opt { if !webhook_supports_event(&config, &event_type) { @@ -130,7 +130,7 @@ pub(crate) fn emit_subscription_event( created_at: env.ledger().timestamp(), updated_at: env.ledger().timestamp(), }; - storage_persistent_set(env, storage, StorageKeyExt::WebhookDelivery(delivery_id), delivery); + storage_persistent_set(env, storage, StorageKey::WebhookDelivery(delivery_id), delivery); let mut deliveries = deliveries_for_webhook(env, storage, webhook_id); deliveries.push_back(delivery_id); @@ -160,7 +160,7 @@ impl super::SubTrackrSubscription { config.success_count = 0; config.failure_count = 0; - storage_persistent_set(&env, &storage, StorageKeyExt::Webhook(id), config.clone()); + storage_persistent_set(&env, &storage, StorageKey::Webhook(id), config.clone()); let mut ids = webhook_ids_for_merchant(&env, &storage, &config.merchant); ids.push_back(id); @@ -179,7 +179,7 @@ impl super::SubTrackrSubscription { config.merchant.require_auth(); let current: WebhookConfig = - storage_persistent_get(&env, &storage, StorageKeyExt::Webhook(id)) + storage_persistent_get(&env, &storage, StorageKey::Webhook(id)) .expect("Webhook not found"); assert!(current.merchant == config.merchant, "Webhook merchant mismatch"); @@ -191,12 +191,12 @@ impl super::SubTrackrSubscription { config.health_check_at = current.health_check_at; config.healthy = current.healthy; - storage_persistent_set(&env, &storage, StorageKeyExt::Webhook(id), config); + storage_persistent_set(&env, &storage, StorageKey::Webhook(id), config); } pub fn delete_webhook(env: Env, proxy: Address, storage: Address, id: u64) { proxy.require_auth(); - let config: WebhookConfig = storage_persistent_get(&env, &storage, StorageKeyExt::Webhook(id)) + let config: WebhookConfig = storage_persistent_get(&env, &storage, StorageKey::Webhook(id)) .expect("Webhook not found"); config.merchant.require_auth(); @@ -211,31 +211,31 @@ impl super::SubTrackrSubscription { storage_persistent_remove( &env, &storage, - StorageKeyExt::WebhookDeliveriesByWebhook(id), + StorageKey::WebhookDeliveriesByWebhook(id), ); - storage_persistent_remove(&env, &storage, StorageKeyExt::Webhook(id)); + storage_persistent_remove(&env, &storage, StorageKey::Webhook(id)); } pub fn pause_webhook(env: Env, proxy: Address, storage: Address, id: u64) { proxy.require_auth(); let mut config: WebhookConfig = - storage_persistent_get(&env, &storage, StorageKeyExt::Webhook(id)) + storage_persistent_get(&env, &storage, StorageKey::Webhook(id)) .expect("Webhook not found"); config.merchant.require_auth(); config.is_paused = true; config.updated_at = env.ledger().timestamp(); - storage_persistent_set(&env, &storage, StorageKeyExt::Webhook(id), config); + storage_persistent_set(&env, &storage, StorageKey::Webhook(id), config); } pub fn resume_webhook(env: Env, proxy: Address, storage: Address, id: u64) { proxy.require_auth(); let mut config: WebhookConfig = - storage_persistent_get(&env, &storage, StorageKeyExt::Webhook(id)) + storage_persistent_get(&env, &storage, StorageKey::Webhook(id)) .expect("Webhook not found"); config.merchant.require_auth(); config.is_paused = false; config.updated_at = env.ledger().timestamp(); - storage_persistent_set(&env, &storage, StorageKeyExt::Webhook(id), config); + storage_persistent_set(&env, &storage, StorageKey::Webhook(id), config); } pub fn list_webhooks(env: Env, proxy: Address, storage: Address, merchant: Address) -> Vec { @@ -244,7 +244,7 @@ impl super::SubTrackrSubscription { let mut items = Vec::new(&env); for webhook_id in ids.iter() { let config_opt: Option = - storage_persistent_get(&env, &storage, StorageKeyExt::Webhook(webhook_id)); + storage_persistent_get(&env, &storage, StorageKey::Webhook(webhook_id)); if let Some(config) = config_opt { items.push_back(config); } @@ -268,7 +268,7 @@ impl super::SubTrackrSubscription { let delivery_opt: Option = storage_persistent_get( &env, &storage, - StorageKeyExt::WebhookDelivery(delivery_id), + StorageKey::WebhookDelivery(delivery_id), ); if let Some(delivery) = delivery_opt { items.push_back(delivery); @@ -281,13 +281,13 @@ impl super::SubTrackrSubscription { pub fn retry_webhook_delivery(env: Env, proxy: Address, storage: Address, delivery_id: u64) { proxy.require_auth(); let mut delivery: WebhookDelivery = - storage_persistent_get(&env, &storage, StorageKeyExt::WebhookDelivery(delivery_id)) + storage_persistent_get(&env, &storage, StorageKey::WebhookDelivery(delivery_id)) .expect("Webhook delivery not found"); let config: WebhookConfig = storage_persistent_get( &env, &storage, - StorageKeyExt::Webhook(delivery.webhook_id), + StorageKey::Webhook(delivery.webhook_id), ) .expect("Webhook not found"); config.merchant.require_auth(); @@ -302,7 +302,7 @@ impl super::SubTrackrSubscription { + compute_delay(&config.retry_policy, delivery.attempts); } delivery.updated_at = env.ledger().timestamp(); - storage_persistent_set(&env, &storage, StorageKeyExt::WebhookDelivery(delivery_id), delivery); + storage_persistent_set(&env, &storage, StorageKey::WebhookDelivery(delivery_id), delivery); } pub fn get_webhook_health( @@ -313,7 +313,7 @@ impl super::SubTrackrSubscription { ) -> WebhookConfig { proxy.require_auth(); let mut config: WebhookConfig = - storage_persistent_get(&env, &storage, StorageKeyExt::Webhook(webhook_id)) + storage_persistent_get(&env, &storage, StorageKey::Webhook(webhook_id)) .expect("Webhook not found"); config.merchant.require_auth(); @@ -324,7 +324,7 @@ impl super::SubTrackrSubscription { let delivery_opt: Option = storage_persistent_get( &env, &storage, - StorageKeyExt::WebhookDelivery(delivery_id), + StorageKey::WebhookDelivery(delivery_id), ); if let Some(delivery) = delivery_opt { match delivery.status { @@ -338,7 +338,7 @@ impl super::SubTrackrSubscription { config.healthy = failures <= successes; config.health_check_at = env.ledger().timestamp(); config.updated_at = config.health_check_at; - storage_persistent_set(&env, &storage, StorageKeyExt::Webhook(webhook_id), config.clone()); + storage_persistent_set(&env, &storage, StorageKey::Webhook(webhook_id), config.clone()); config } } diff --git a/contracts/types/src/lib.rs b/contracts/types/src/lib.rs index a6c8233d..912fd042 100644 --- a/contracts/types/src/lib.rs +++ b/contracts/types/src/lib.rs @@ -407,4 +407,331 @@ pub enum StorageKey { ChargeCommitment(u64), MevAlertCount, MevAlert(u64), + + // Plan and payment method storage keys + MaxPlansPerMerchant, + UserPaymentMethods(Address), + PaymentMethodEntry(Address, u64), + PaymentMethodCount(Address), +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum Role { + Admin, + Merchant, + Subscriber, + Auditor, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum Permission { + GrantRole, + RevokeRole, + DelegatePermission, + CreatePlan, + DeactivatePlan, + SetPlanQuotas, + SetRevenueRule, + Subscribe, + CancelSubscription, + PauseSubscription, + ResumeSubscription, + ChargeSubscription, + RequestRefund, + ApproveRefund, + RejectRefund, + RequestTransfer, + AcceptTransfer, + SetRateLimit, + RemoveRateLimit, + SetInvoiceContract, + ClearInvoiceContract, + UpgradeContract, + MigrateContract, + ViewAnalytics, + ViewAuditLog, + ViewPlans, + ViewSubscriptions, + SetEmergencyAdmin, + PauseEmergency, + SetAccessControl, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum RoleChangeAction { + Granted, + Revoked, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct RoleChangeEntry { + pub id: u64, + pub user: Address, + pub role: Role, + pub action: RoleChangeAction, + pub changed_by: Address, + pub timestamp: Timestamp, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct PriceBounds { + pub min_price_bps: u32, + pub max_price_bps: u32, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct BillingSchedule { + pub subscription_id: u64, + pub interval: Interval, + pub start_date: u64, + pub custom_invoice_day: u32, + pub promotional_duration_days: u32, + pub promotional_rate: i128, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum ChargeStatus { + Pending, + Attempting, + Completed, + Failed, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct ChargeAttempt { + pub id: u64, + pub subscription_id: u64, + pub status: ChargeStatus, + pub amount: i128, + pub attempted_at: u64, + pub completed_at: u64, + pub error_message: String, + pub retry_count: u32, + pub max_retries: u32, + pub next_retry_at: u64, + pub circuit_breaker_until: u64, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct RetryConfig { + pub max_retries: u32, + pub base_delay_secs: u64, + pub max_delay_secs: u64, + pub backoff_factor: u32, + pub circuit_breaker_threshold: u32, + pub circuit_breaker_cooldown_secs: u64, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum WebhookEventType { + SubscriptionCreated, + SubscriptionUpdated, + SubscriptionCancelled, + SubscriptionRenewed, + PaymentFailed, + ChargeSucceeded, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct WebhookSubscriptionSnapshot { + pub id: u64, + pub plan_id: u64, + pub subscriber: Address, + pub status: SubscriptionStatus, + pub started_at: u64, + pub last_charged_at: u64, + pub next_charge_at: u64, + pub total_paid: i128, + pub total_gas_spent: u64, + pub charge_count: u32, + pub paused_at: u64, + pub pause_duration: u64, + pub refund_requested_amount: i128, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct WebhookPlanSnapshot { + pub id: u64, + pub merchant: Address, + pub name: String, + pub price: i128, + pub token: Address, + pub interval: Interval, + pub active: bool, + pub subscriber_count: u32, + pub created_at: u64, } + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct WebhookEventPayload { + pub id: u64, + pub webhook_id: u64, + pub event_type: WebhookEventType, + pub merchant: Address, + pub occurred_at: u64, + pub subscription: WebhookSubscriptionSnapshot, + pub plan: WebhookPlanSnapshot, + pub previous_status: SubscriptionStatus, + pub current_status: SubscriptionStatus, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct LoyaltyTierConfig { + pub name: String, + pub min_points: u64, + pub discount_bps: u32, + pub multiplier: u32, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct LoyaltyConfig { + pub enabled: bool, + pub points_per_stroop: u64, + pub streak_bonus_pct: u32, + pub referral_points: u64, + pub min_redemption_points: u64, + pub points_expiry_days: u32, + pub tiers: Vec, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum PointTxType { + Earned, + Redeemed, + Expired, + Bonus, + Referral, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct PointTransaction { + pub id: u64, + pub subscriber: Address, + pub tx_type: PointTxType, + pub amount: u64, + pub timestamp: u64, + pub description: String, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct RewardsRedemption { + pub id: u64, + pub subscriber: Address, + pub points_used: u64, + pub discount_applied: i128, + pub redeemed_at: u64, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum TokenType { + StellarAsset, + NativeXLM, + Custom, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum PaymentPriority { + Primary, + Backup, + Fallback, +} + +pub type PaymentMethodId = u64; + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct PaymentMethod { + pub id: u64, + pub user: Address, + pub token_type: TokenType, + pub token_address: Address, + pub chain_id: u64, + pub label: String, + pub priority: PaymentPriority, + pub max_spend_per_interval: i128, + pub is_verified: bool, + pub is_active: bool, + pub expires_at: u64, + pub last_used_at: u64, + pub created_at: u64, + pub updated_at: u64, + pub metadata: Vec, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum TemplateKey { + Template(u64), + TemplateCount, + MerchantTemplates(Address), + SharedTemplates, + TemplateAnalytics(u64), +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum WebhookDeliveryStatus { + Pending, + Delivered, + Failed, + Retrying, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct WebhookRetryPolicy { + pub max_retries: u32, + pub backoff_seconds: u64, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct WebhookConfig { + pub id: u64, + pub merchant: Address, + pub url: String, + pub secret: String, + pub events: Vec, + pub is_paused: bool, + pub created_at: u64, + pub failure_count: u32, + pub retry_policy: WebhookRetryPolicy, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct WebhookDelivery { + pub id: u64, + pub webhook_id: u64, + pub event_type: WebhookEventType, + pub payload_hash: String, + pub status: WebhookDeliveryStatus, + pub attempts: u32, + pub next_attempt_at: u64, + pub last_attempt_at: u64, + pub response_status: u32, +} + + + + diff --git a/package-lock.json b/package-lock.json index bc88c2e0..25eea3c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,6 @@ "": { "name": "subtrackr", "version": "1.0.0", - "hasInstallScript": true, "dependencies": { "@react-native-async-storage/async-storage": "2.1.2", "@react-native-community/datetimepicker": "^9.1.0", @@ -16,26 +15,26 @@ "@react-navigation/native": "^6.1.9", "@react-navigation/native-stack": "^6.9.17", "@reown/appkit-ethers-react-native": "^1.3.0", - "@sentry/react-native": "^5.4.0", "@shopify/flash-list": "latest", "@stellar/stellar-sdk": "^12.0.0", "@superfluid-finance/sdk-core": "^0.9.0", + "@testing-library/react-hooks": "^8.0.1", "@walletconnect/react-native-compat": "^2.23.9", "@walletconnect/utils": "^2.23.9", "ethers": "^5.8.0", "expo": "~53.0.20", "expo-application": "~6.1.5", - "expo-build-properties": "^0.12.5", "expo-clipboard": "~7.1.5", "expo-dev-client": "~5.2.4", + "expo-haptics": "~14.1.4", "expo-image": "~2.3.0", - "expo-linking": "~7.1.7", "expo-notifications": "^0.31.5", "expo-status-bar": "~2.2.3", + "graphql": "^16.13.2", "i18next": "^26.0.8", "react": "19.2.5", "react-i18next": "^17.0.6", - "react-native": "0.79.7", + "react-native": "0.85.2", "react-native-gesture-handler": "~2.31.1", "react-native-get-random-values": "~1.11.0", "react-native-modal": "14.0.0-rc.1", @@ -62,7 +61,6 @@ "@semantic-release/npm": "^12.0.2", "@semantic-release/release-notes-generator": "^14.1.0", "@size-limit/file": "^11.1.4", - "@testing-library/react-native": "13.3.3", "@typechain/ethers-v5": "^11.1.2", "@types/detox": "^17.14.3", "@types/jest": "^29.5.14", @@ -75,7 +73,6 @@ "eslint": "^8.57.0", "eslint-config-expo": "^7.0.0", "eslint-plugin-prettier": "^5.1.3", - "graphql": "^16.13.2", "husky": "^9.1.7", "jest": "^29.7.0", "jest-circus": "^30.3.0", @@ -572,6 +569,7 @@ "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -584,6 +582,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -596,6 +595,7 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" @@ -608,6 +608,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -680,6 +681,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" @@ -695,6 +697,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -707,6 +710,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -734,6 +738,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -758,6 +763,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -770,6 +776,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -782,6 +789,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -806,6 +814,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -821,6 +830,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -1524,25 +1534,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/traverse--for-generate-function-map": { - "name": "@babel/traverse", - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/types": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", @@ -3721,6 +3712,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, "license": "ISC", "dependencies": { "camelcase": "^5.3.1", @@ -3737,6 +3729,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" @@ -3746,6 +3739,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3755,6 +3749,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -3768,6 +3763,7 @@ "version": "3.14.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -3781,6 +3777,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -3793,6 +3790,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -3808,6 +3806,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -3820,6 +3819,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3908,6 +3908,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3" @@ -4909,6 +4910,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -6107,12 +6109,12 @@ } }, "node_modules/@react-native/assets-registry": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.7.tgz", - "integrity": "sha512-YeOXq8H5JZQbeIcAtHxmboDt02QG8ej8Z4SFVNh5UjaSb/0X1/v5/DhwNb4dfpIsQ5lFy75jeoSmUVp8qEKu9g==", + "version": "0.85.2", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.85.2.tgz", + "integrity": "sha512-kauC/oPaxklU4Y+u9gBfCBJm51qX6WBZq4xx0USCdimtp+G8+554kpygfSWIjoqCJa2o06bWxBEjesiuCv+LzA==", "license": "MIT", "engines": { - "node": ">=18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/@react-native/babel-plugin-codegen": { @@ -6187,6 +6189,30 @@ "@babel/core": "*" } }, + "node_modules/@react-native/babel-preset/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.25.1.tgz", + "integrity": "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.25.1" + } + }, + "node_modules/@react-native/babel-preset/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/@react-native/babel-preset/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/@react-native/codegen": { "version": "0.79.6", "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.79.6.tgz", @@ -6239,6 +6265,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@react-native/codegen/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/@react-native/codegen/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/@react-native/codegen/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -6252,78 +6293,80 @@ } }, "node_modules/@react-native/community-cli-plugin": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.79.7.tgz", - "integrity": "sha512-UQADqWfnKfEGMIyOa1zI8TMAOOLDdQ3h2FTCG8bp+MFGLAaJowaa+4GGb71A26fbg06/qnGy/Kr0Mv41IFGZnQ==", + "version": "0.85.2", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.85.2.tgz", + "integrity": "sha512-3KLgSg1kHvBpr93zMaQhvfYTgnCw7yZRED+3J4dMcYjfSjtD0Wf8SofU6uBmAw9JaVYvP43lpdwUpI4p0+ABsg==", "license": "MIT", "dependencies": { - "@react-native/dev-middleware": "0.79.7", - "chalk": "^4.0.0", - "debug": "^2.2.0", + "@react-native/dev-middleware": "0.85.2", + "debug": "^4.4.0", "invariant": "^2.2.4", - "metro": "^0.82.0", - "metro-config": "^0.82.0", - "metro-core": "^0.82.0", + "metro": "^0.84.0", + "metro-config": "^0.84.0", + "metro-core": "^0.84.0", "semver": "^7.1.3" }, "engines": { - "node": ">=18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" }, "peerDependencies": { - "@react-native-community/cli": "*" + "@react-native-community/cli": "*", + "@react-native/metro-config": "0.85.2" }, "peerDependenciesMeta": { "@react-native-community/cli": { "optional": true + }, + "@react-native/metro-config": { + "optional": true } } }, "node_modules/@react-native/community-cli-plugin/node_modules/@react-native/debugger-frontend": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.79.7.tgz", - "integrity": "sha512-91JVlhR6hDuJXcWTpCwcdEPlUQf+TckNG8BYfR4UkUOaZ87XahJv4EyWBeyfd8lwB/mh6nDJqbR6UiXwt5kbog==", + "version": "0.85.2", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.85.2.tgz", + "integrity": "sha512-j+0b9H5f5hGTLQxHIhJU/b/W6ijuxJF+ZTLHB0se2kzUBNxFKd7DkIc6753qk3CJdiv55vxG3XDgmlpbHxOpmA==", "license": "BSD-3-Clause", "engines": { - "node": ">=18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/@react-native/community-cli-plugin/node_modules/@react-native/dev-middleware": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.79.7.tgz", - "integrity": "sha512-KHGPa7xwnKKWrzMnV1cHc8J56co4tFevmRvbjEbUCqkGS0s/l8ZxAGMR222/6YxZV3Eg1J3ywKQ8nHzTsTz5jw==", + "version": "0.85.2", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.85.2.tgz", + "integrity": "sha512-3J+NaDUg+QEfDeLAUzgaWhpaxEg78g+KwbydlDCewh2G6WnHpsty8XooruxNHzyAsqVWywZMrzmbn78Ctc1O9Q==", "license": "MIT", "dependencies": { "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.79.7", + "@react-native/debugger-frontend": "0.85.2", + "@react-native/debugger-shell": "0.85.2", "chrome-launcher": "^0.15.2", - "chromium-edge-launcher": "^0.2.0", + "chromium-edge-launcher": "^0.3.0", "connect": "^3.6.5", - "debug": "^2.2.0", + "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", - "ws": "^6.2.3" + "ws": "^7.5.10" }, "engines": { - "node": ">=18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, - "node_modules/@react-native/community-cli-plugin/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", + "node_modules/@react-native/community-cli-plugin/node_modules/chromium-edge-launcher": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.3.0.tgz", + "integrity": "sha512-p03azHlGjtyRvFEee3cyvtsRYdniSkwjkzmM/KmVnqT5d7QkkwpJBhis/zCLMYdQMVJ5tt140TBNqqrZPaWeFA==", + "license": "Apache-2.0", "dependencies": { - "ms": "2.0.0" + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4" } }, - "node_modules/@react-native/community-cli-plugin/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/@react-native/community-cli-plugin/node_modules/open": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", @@ -6341,9 +6384,9 @@ } }, "node_modules/@react-native/community-cli-plugin/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6353,12 +6396,24 @@ } }, "node_modules/@react-native/community-cli-plugin/node_modules/ws": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.4.tgz", - "integrity": "sha512-PNIUUyLI5YpkJZj60YBzX1o0ByQ4ovvfmq9N/Kig/PAYbVlGyz4R6G0SEWrD0O9acc0sT2+IdMBVLFv8FSi0Nw==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", "license": "MIT", - "dependencies": { - "async-limiter": "~1.0.0" + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/@react-native/debugger-frontend": { @@ -6370,6 +6425,20 @@ "node": ">=18" } }, + "node_modules/@react-native/debugger-shell": { + "version": "0.85.2", + "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.85.2.tgz", + "integrity": "sha512-r5BkhqPMfg3LmaZS5zadHmBNVH5h4bhSpv4BEPGfK4gat9HABAMzUzybi+2wpgU3SoHxnyKGdExEJvoqVcjeRg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6", + "debug": "^4.4.0", + "fb-dotslash": "0.5.8" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, "node_modules/@react-native/dev-middleware": { "version": "0.79.6", "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.79.6.tgz", @@ -6433,12 +6502,12 @@ } }, "node_modules/@react-native/gradle-plugin": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.79.7.tgz", - "integrity": "sha512-vQqVthSs2EGqzV4KI0uFr/B4hUVXhVM86ekYL8iZCXzO6bewZa7lEUNGieijY0jc0a/mBJ6KZDzMtcUoS5vFRA==", + "version": "0.85.2", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.85.2.tgz", + "integrity": "sha512-YXBOLeAqFrv7XwUeBPTKZeOV1FIxn4AW7UAEitScf3ibC8bu8+6NpJu4HWgbNQHg7vDbbTZVbcOl8EwGxsSq2w==", "license": "MIT", "engines": { - "node": ">=18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/@react-native/jest-preset": { @@ -6465,7 +6534,6 @@ "version": "0.85.2", "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.85.2.tgz", "integrity": "sha512-esGEAmKVM40DV/yVmNljCKZTIeUo7qXqc+Hwffkv3TG+b3E24xyFovHrbP98gGxZr2ZsEyx+2sKLdXF5asY5nw==", - "dev": true, "license": "MIT", "engines": { "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" @@ -6478,21 +6546,21 @@ "license": "MIT" }, "node_modules/@react-native/virtualized-lists": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.7.tgz", - "integrity": "sha512-CPJ995n1WIyi7KeLj+/aeFCe6MWQrRRXfMvBnc7XP4noSa4WEJfH8Zcvl/iWYVxrQdIaInadoiYLakeSflz5jg==", + "version": "0.85.2", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.85.2.tgz", + "integrity": "sha512-wmVKpAlcr+UB0L5SpbrV865EdleUP7I5+X+48e1aRsQK8q+wsTRBXeUwWVip/1l+HZwlZFeO8iOILJ16VRu0Cw==", "license": "MIT", "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "engines": { - "node": ">=18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" }, "peerDependencies": { - "@types/react": "^19.0.0", + "@types/react": "^19.2.0", "react": "*", - "react-native": "*" + "react-native": "0.85.2" }, "peerDependenciesMeta": { "@types/react": { @@ -8449,371 +8517,6 @@ "semantic-release": ">=20.1.0" } }, - "node_modules/@sentry-internal/feedback": { - "version": "7.119.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.119.1.tgz", - "integrity": "sha512-EPyW6EKZmhKpw/OQUPRkTynXecZdYl4uhZwdZuGqnGMAzswPOgQvFrkwsOuPYvoMfXqCH7YuRqyJrox3uBOrTA==", - "license": "MIT", - "dependencies": { - "@sentry/core": "7.119.1", - "@sentry/types": "7.119.1", - "@sentry/utils": "7.119.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@sentry-internal/replay-canvas": { - "version": "7.119.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.119.1.tgz", - "integrity": "sha512-O/lrzENbMhP/UDr7LwmfOWTjD9PLNmdaCF408Wx8SDuj7Iwc+VasGfHg7fPH4Pdr4nJON6oh+UqoV4IoG05u+A==", - "license": "MIT", - "dependencies": { - "@sentry/core": "7.119.1", - "@sentry/replay": "7.119.1", - "@sentry/types": "7.119.1", - "@sentry/utils": "7.119.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@sentry-internal/tracing": { - "version": "7.119.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.119.1.tgz", - "integrity": "sha512-cI0YraPd6qBwvUA3wQdPGTy8PzAoK0NZiaTN1LM3IczdPegehWOaEG5GVTnpGnTsmBAzn1xnBXNBhgiU4dgcrQ==", - "license": "MIT", - "dependencies": { - "@sentry/core": "7.119.1", - "@sentry/types": "7.119.1", - "@sentry/utils": "7.119.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/babel-plugin-component-annotate": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.20.1.tgz", - "integrity": "sha512-4mhEwYTK00bIb5Y9UWIELVUfru587Vaeg0DQGswv4aIRHIiMKLyNqCEejaaybQ/fNChIZOKmvyqXk430YVd7Qg==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/@sentry/browser": { - "version": "7.119.1", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.119.1.tgz", - "integrity": "sha512-aMwAnFU4iAPeLyZvqmOQaEDHt/Dkf8rpgYeJ0OEi50dmP6AjG+KIAMCXU7CYCCQDn70ITJo8QD5+KzCoZPYz0A==", - "license": "MIT", - "dependencies": { - "@sentry-internal/feedback": "7.119.1", - "@sentry-internal/replay-canvas": "7.119.1", - "@sentry-internal/tracing": "7.119.1", - "@sentry/core": "7.119.1", - "@sentry/integrations": "7.119.1", - "@sentry/replay": "7.119.1", - "@sentry/types": "7.119.1", - "@sentry/utils": "7.119.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/browser/node_modules/@sentry/integrations": { - "version": "7.119.1", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.119.1.tgz", - "integrity": "sha512-CGmLEPnaBqbUleVqrmGYjRjf5/OwjUXo57I9t0KKWViq81mWnYhaUhRZWFNoCNQHns+3+GPCOMvl0zlawt+evw==", - "license": "MIT", - "dependencies": { - "@sentry/core": "7.119.1", - "@sentry/types": "7.119.1", - "@sentry/utils": "7.119.1", - "localforage": "^1.8.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/cli": { - "version": "2.37.0", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.37.0.tgz", - "integrity": "sha512-fM3V4gZRJR/s8lafc3O07hhOYRnvkySdPkvL/0e0XW0r+xRwqIAgQ5ECbsZO16A5weUiXVSf03ztDL1FcmbJCQ==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.7", - "progress": "^2.0.3", - "proxy-from-env": "^1.1.0", - "which": "^2.0.2" - }, - "bin": { - "sentry-cli": "bin/sentry-cli" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@sentry/cli-darwin": "2.37.0", - "@sentry/cli-linux-arm": "2.37.0", - "@sentry/cli-linux-arm64": "2.37.0", - "@sentry/cli-linux-i686": "2.37.0", - "@sentry/cli-linux-x64": "2.37.0", - "@sentry/cli-win32-i686": "2.37.0", - "@sentry/cli-win32-x64": "2.37.0" - } - }, - "node_modules/@sentry/cli-darwin": { - "version": "2.37.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.37.0.tgz", - "integrity": "sha512-CsusyMvO0eCPSN7H+sKHXS1pf637PWbS4rZak/7giz/z31/6qiXmeMlcL3f9lLZKtFPJmXVFO9uprn1wbBVF8A==", - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-arm": { - "version": "2.37.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.37.0.tgz", - "integrity": "sha512-Dz0qH4Yt+gGUgoVsqVt72oDj4VQynRF1QB1/Sr8g76Vbi+WxWZmUh0iFwivYVwWxdQGu/OQrE0tx946HToCRyA==", - "cpu": [ - "arm" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-arm64": { - "version": "2.37.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.37.0.tgz", - "integrity": "sha512-2vzUWHLZ3Ct5gpcIlfd/2Qsha+y9M8LXvbZE26VxzYrIkRoLAWcnClBv8m4XsHLMURYvz3J9QSZHMZHSO7kAzw==", - "cpu": [ - "arm64" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-i686": { - "version": "2.37.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.37.0.tgz", - "integrity": "sha512-MHRLGs4t/CQE1pG+mZBQixyWL6xDZfNalCjO8GMcTTbZFm44S3XRHfYJZNVCgdtnUP7b6OHGcu1v3SWE10LcwQ==", - "cpu": [ - "x86", - "ia32" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-x64": { - "version": "2.37.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.37.0.tgz", - "integrity": "sha512-k76ClefKZaDNJZU/H3mGeR8uAzAGPzDRG/A7grzKfBeyhP3JW09L7Nz9IQcSjCK+xr399qLhM2HFCaPWQ6dlMw==", - "cpu": [ - "x64" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-win32-i686": { - "version": "2.37.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.37.0.tgz", - "integrity": "sha512-FFyi5RNYQQkEg4GkP2f3BJcgQn0F4fjFDMiWkjCkftNPXQG+HFUEtrGsWr6mnHPdFouwbYg3tEPUWNxAoypvTw==", - "cpu": [ - "x86", - "ia32" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-win32-x64": { - "version": "2.37.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.37.0.tgz", - "integrity": "sha512-nSMj4OcfQmyL+Tu/jWCJwhKCXFsCZW1MUk6wjjQlRt9SDLfgeapaMlK1ZvT1eZv5ZH6bj3qJfefwj4U8160uOA==", - "cpu": [ - "x64" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/@sentry/cli/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@sentry/core": { - "version": "7.119.1", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.119.1.tgz", - "integrity": "sha512-YUNnH7O7paVd+UmpArWCPH4Phlb5LwrkWVqzFWqL3xPyCcTSof2RL8UmvpkTjgYJjJ+NDfq5mPFkqv3aOEn5Sw==", - "license": "MIT", - "dependencies": { - "@sentry/types": "7.119.1", - "@sentry/utils": "7.119.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/hub": { - "version": "7.119.0", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-7.119.0.tgz", - "integrity": "sha512-183h5B/rZosLxpB+ZYOvFdHk0rwZbKskxqKFtcyPbDAfpCUgCass41UTqyxF6aH1qLgCRxX8GcLRF7frIa/SOg==", - "license": "MIT", - "dependencies": { - "@sentry/core": "7.119.0", - "@sentry/types": "7.119.0", - "@sentry/utils": "7.119.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/hub/node_modules/@sentry/core": { - "version": "7.119.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.119.0.tgz", - "integrity": "sha512-CS2kUv9rAJJEjiRat6wle3JATHypB0SyD7pt4cpX5y0dN5dZ1JrF57oLHRMnga9fxRivydHz7tMTuBhSSwhzjw==", - "license": "MIT", - "dependencies": { - "@sentry/types": "7.119.0", - "@sentry/utils": "7.119.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/hub/node_modules/@sentry/types": { - "version": "7.119.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.119.0.tgz", - "integrity": "sha512-27qQbutDBPKGbuJHROxhIWc1i0HJaGLA90tjMu11wt0E4UNxXRX+UQl4Twu68v4EV3CPvQcEpQfgsViYcXmq+w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/hub/node_modules/@sentry/utils": { - "version": "7.119.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.119.0.tgz", - "integrity": "sha512-ZwyXexWn2ZIe2bBoYnXJVPc2esCSbKpdc6+0WJa8eutXfHq3FRKg4ohkfCBpfxljQGEfP1+kfin945lA21Ka+A==", - "license": "MIT", - "dependencies": { - "@sentry/types": "7.119.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/integrations": { - "version": "7.119.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.119.0.tgz", - "integrity": "sha512-OHShvtsRW0A+ZL/ZbMnMqDEtJddPasndjq+1aQXw40mN+zeP7At/V1yPZyFaURy86iX7Ucxw5BtmzuNy7hLyTA==", - "license": "MIT", - "dependencies": { - "@sentry/core": "7.119.0", - "@sentry/types": "7.119.0", - "@sentry/utils": "7.119.0", - "localforage": "^1.8.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/integrations/node_modules/@sentry/core": { - "version": "7.119.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.119.0.tgz", - "integrity": "sha512-CS2kUv9rAJJEjiRat6wle3JATHypB0SyD7pt4cpX5y0dN5dZ1JrF57oLHRMnga9fxRivydHz7tMTuBhSSwhzjw==", - "license": "MIT", - "dependencies": { - "@sentry/types": "7.119.0", - "@sentry/utils": "7.119.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/integrations/node_modules/@sentry/types": { - "version": "7.119.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.119.0.tgz", - "integrity": "sha512-27qQbutDBPKGbuJHROxhIWc1i0HJaGLA90tjMu11wt0E4UNxXRX+UQl4Twu68v4EV3CPvQcEpQfgsViYcXmq+w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/integrations/node_modules/@sentry/utils": { - "version": "7.119.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.119.0.tgz", - "integrity": "sha512-ZwyXexWn2ZIe2bBoYnXJVPc2esCSbKpdc6+0WJa8eutXfHq3FRKg4ohkfCBpfxljQGEfP1+kfin945lA21Ka+A==", - "license": "MIT", - "dependencies": { - "@sentry/types": "7.119.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@sentry/minimal": { "version": "5.30.0", "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.30.0.tgz", @@ -8961,70 +8664,6 @@ "node": ">= 6" } }, - "node_modules/@sentry/react": { - "version": "7.119.1", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.119.1.tgz", - "integrity": "sha512-Bri314LnSVm16K3JATgn3Zsq6Uj3M/nIjdUb3nggBw0BMlFWMsyFjUCfmCio5d80KJK/lUjOIxRjzu79M6jOzQ==", - "license": "MIT", - "dependencies": { - "@sentry/browser": "7.119.1", - "@sentry/core": "7.119.1", - "@sentry/types": "7.119.1", - "@sentry/utils": "7.119.1", - "hoist-non-react-statics": "^3.3.2" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "react": "15.x || 16.x || 17.x || 18.x" - } - }, - "node_modules/@sentry/react-native": { - "version": "5.36.0", - "resolved": "https://registry.npmjs.org/@sentry/react-native/-/react-native-5.36.0.tgz", - "integrity": "sha512-MPTN5Wb6wEplIVydh2oXOdLJYqCAWKvncN5TBPN5OG8XdCsDqF7LyH2Sz+SK2T3hMPKESl3StAMhrrNSmHDbNg==", - "license": "MIT", - "dependencies": { - "@sentry/babel-plugin-component-annotate": "2.20.1", - "@sentry/browser": "7.119.1", - "@sentry/cli": "2.37.0", - "@sentry/core": "7.119.1", - "@sentry/hub": "7.119.0", - "@sentry/integrations": "7.119.0", - "@sentry/react": "7.119.1", - "@sentry/types": "7.119.1", - "@sentry/utils": "7.119.1" - }, - "bin": { - "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" - }, - "peerDependencies": { - "expo": ">=49.0.0", - "react": ">=17.0.0", - "react-native": ">=0.65.0" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - } - } - }, - "node_modules/@sentry/replay": { - "version": "7.119.1", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.119.1.tgz", - "integrity": "sha512-4da+ruMEipuAZf35Ybt2StBdV1S+oJbSVccGpnl9w6RoeQoloT4ztR6ML3UcFDTXeTPT1FnHWDCyOfST0O7XMw==", - "license": "MIT", - "dependencies": { - "@sentry-internal/tracing": "7.119.1", - "@sentry/core": "7.119.1", - "@sentry/types": "7.119.1", - "@sentry/utils": "7.119.1" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@sentry/tracing": { "version": "5.30.0", "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.30.0.tgz", @@ -9077,27 +8716,6 @@ "node": ">=6" } }, - "node_modules/@sentry/types": { - "version": "7.119.1", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.119.1.tgz", - "integrity": "sha512-4G2mcZNnYzK3pa2PuTq+M2GcwBRY/yy1rF+HfZU+LAPZr98nzq2X3+mJHNJoobeHRkvVh7YZMPi4ogXiIS5VNQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/utils": { - "version": "7.119.1", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.119.1.tgz", - "integrity": "sha512-ju/Cvyeu/vkfC5/XBV30UNet5kLEicZmXSyuLwZu95hEbL+foPdxN+re7pCI/eNqfe3B2vz7lvz5afLVOlQ2Hg==", - "license": "MIT", - "dependencies": { - "@sentry/types": "7.119.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@shopify/flash-list": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-2.3.1.tgz", @@ -9197,6 +8815,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" @@ -9386,82 +9005,36 @@ "node": ">=14.16" } }, - "node_modules/@testing-library/react-native": { - "version": "13.3.3", - "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-13.3.3.tgz", - "integrity": "sha512-k6Mjsd9dbZgvY4Bl7P1NIpePQNi+dfYtlJ5voi9KQlynxSyQkfOgJmYGCYmw/aSgH/rUcFvG8u5gd4npzgRDyg==", - "dev": true, + "node_modules/@testing-library/react-hooks": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz", + "integrity": "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==", "license": "MIT", "dependencies": { - "jest-matcher-utils": "^30.0.5", - "picocolors": "^1.1.1", - "pretty-format": "^30.0.5", - "redent": "^3.0.0" + "@babel/runtime": "^7.12.5", + "react-error-boundary": "^3.1.0" }, "engines": { - "node": ">=18" + "node": ">=12" }, "peerDependencies": { - "jest": ">=29.0.0", - "react": ">=18.2.0", - "react-native": ">=0.71", - "react-test-renderer": ">=18.2.0" + "@types/react": "^16.9.0 || ^17.0.0", + "react": "^16.9.0 || ^17.0.0", + "react-dom": "^16.9.0 || ^17.0.0", + "react-test-renderer": "^16.9.0 || ^17.0.0" }, "peerDependenciesMeta": { - "jest": { + "@types/react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-test-renderer": { "optional": true } } }, - "node_modules/@testing-library/react-native/node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@testing-library/react-native/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/react-native/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/react-native/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/@tootallnate/once": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", @@ -10086,6 +9659,7 @@ "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", @@ -10099,6 +9673,7 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -10108,6 +9683,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -10118,6 +9694,7 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.2" @@ -10155,6 +9732,7 @@ "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -10323,6 +9901,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, "license": "MIT" }, "node_modules/@types/tough-cookie": { @@ -12417,6 +11996,7 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -13016,6 +12596,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, "license": "MIT", "dependencies": { "@jest/transform": "^29.7.0", @@ -13037,6 +12618,7 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -13053,6 +12635,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", @@ -13131,6 +12714,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", @@ -13190,10 +12774,35 @@ } } }, + "node_modules/babel-preset-expo/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.25.1.tgz", + "integrity": "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.25.1" + } + }, + "node_modules/babel-preset-expo/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/babel-preset-expo/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/babel-preset-jest": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, "license": "MIT", "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", @@ -13958,39 +13567,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", - "license": "MIT", - "dependencies": { - "callsites": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/caller-callsite/node_modules/callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", - "license": "MIT", - "dependencies": { - "caller-callsite": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -14997,10 +14573,9 @@ } }, "node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "license": "MIT", "engines": { "node": ">=18" @@ -17514,6 +17089,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -18066,31 +17642,6 @@ "react-native": "*" } }, - "node_modules/expo-build-properties": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-0.12.5.tgz", - "integrity": "sha512-donC1le0PYfLKCPKRMGQoixuWuwDWCngzXSoQXUPsgHTDHQUKr8aw+lcWkTwZcItgNovcnk784I0dyfYDcxybA==", - "license": "MIT", - "dependencies": { - "ajv": "^8.11.0", - "semver": "^7.6.0" - }, - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo-build-properties/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/expo-clipboard": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-7.1.5.tgz", @@ -18207,6 +17758,15 @@ "react": "*" } }, + "node_modules/expo-haptics": { + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-14.1.4.tgz", + "integrity": "sha512-QZdE3NMX74rTuIl82I+n12XGwpDWKb8zfs5EpwsnGi/D/n7O2Jd4tO5ivH+muEG/OCJOMq5aeaVDqqaQOhTkcA==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-image": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-2.3.2.tgz", @@ -18240,20 +17800,6 @@ "react": "*" } }, - "node_modules/expo-linking": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-7.1.7.tgz", - "integrity": "sha512-ZJaH1RIch2G/M3hx2QJdlrKbYFUTOjVVW4g39hfxrE5bPX9xhZUYXqxqQtzMNl1ylAevw9JkgEfWbBWddbZ3UA==", - "license": "MIT", - "dependencies": { - "expo-constants": "~17.1.7", - "invariant": "^2.2.4" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/expo-manifests": { "version": "0.16.6", "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.16.6.tgz", @@ -18661,6 +18207,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, "funding": [ { "type": "github", @@ -18723,6 +18270,18 @@ "reusify": "^1.0.4" } }, + "node_modules/fb-dotslash": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz", + "integrity": "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "dotslash": "bin/dotslash" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -18736,7 +18295,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -19286,6 +18844,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.0.0" @@ -19587,7 +19146,6 @@ "version": "16.14.1", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.1.tgz", "integrity": "sha512-cQOsSMS/IrDz82PVyRDvf/Q1F/bRbBVjJlh+xYOkI1qw2bWRvWGiWc+m2O0d6l4Bt1fyY+8kzJ8JFWGJqNeDBg==", - "dev": true, "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" @@ -20321,6 +19879,12 @@ "upper-case": "^1.1.3" } }, + "node_modules/hermes-compiler": { + "version": "250829098.0.10", + "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.10.tgz", + "integrity": "sha512-TcRlZ0/TlyfJqquRFAWoyElVNnkdYRi/sEp4/Qy8/GYxjg8j2cS9D4MjuaQ+qimkmLN7AmO+44IznRf06mAr0w==", + "license": "MIT" + }, "node_modules/hermes-estree": { "version": "0.33.3", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", @@ -20703,27 +20267,6 @@ "node": ">= 4" } }, - "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, "node_modules/immutable": { "version": "4.3.8", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", @@ -20806,6 +20349,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -21106,15 +20650,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -21610,6 +21145,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=8" @@ -21619,6 +21155,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", @@ -23022,6 +22559,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -23039,6 +22577,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, "license": "MIT", "dependencies": { "@jest/fake-timers": "^29.7.0", @@ -23054,6 +22593,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -23071,6 +22611,7 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" @@ -23080,6 +22621,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -23161,6 +22703,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -23265,6 +22808,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.13", @@ -23404,6 +22948,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -24257,6 +23802,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { @@ -24460,15 +24006,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/lighthouse-logger": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", @@ -24962,15 +24499,6 @@ "node": ">=4" } }, - "node_modules/localforage": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", - "license": "Apache-2.0", - "dependencies": { - "lie": "3.1.1" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -25761,45 +25289,43 @@ } }, "node_modules/metro": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.82.5.tgz", - "integrity": "sha512-8oAXxL7do8QckID/WZEKaIFuQJFUTLzfVcC48ghkHhNK2RGuQq8Xvf4AVd+TUA0SZtX0q8TGNXZ/eba1ckeGCg==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.5.tgz", + "integrity": "sha512-r1liLkyFZMVSEMNjU1CJU5pRzs3NdkxHqXS60O25c0rCIqAR+cGk7rPydw/g0WAIKVXojIBIF45yYBPagJGcgw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", + "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.3", - "@babel/types": "^7.25.2", - "accepts": "^1.3.7", - "chalk": "^4.0.0", + "@babel/generator": "^7.29.1", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "accepts": "^2.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", - "hermes-parser": "0.29.1", - "image-size": "^1.0.2", + "hermes-parser": "0.35.0", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.82.5", - "metro-cache": "0.82.5", - "metro-cache-key": "0.82.5", - "metro-config": "0.82.5", - "metro-core": "0.82.5", - "metro-file-map": "0.82.5", - "metro-resolver": "0.82.5", - "metro-runtime": "0.82.5", - "metro-source-map": "0.82.5", - "metro-symbolicate": "0.82.5", - "metro-transform-plugins": "0.82.5", - "metro-transform-worker": "0.82.5", - "mime-types": "^2.1.27", + "metro-babel-transformer": "0.84.5", + "metro-cache": "0.84.5", + "metro-cache-key": "0.84.5", + "metro-config": "0.84.5", + "metro-core": "0.84.5", + "metro-file-map": "0.84.5", + "metro-resolver": "0.84.5", + "metro-runtime": "0.84.5", + "metro-source-map": "0.84.5", + "metro-symbolicate": "0.84.5", + "metro-transform-plugins": "0.84.5", + "metro-transform-worker": "0.84.5", + "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", @@ -25811,160 +25337,104 @@ "metro": "src/cli.js" }, "engines": { - "node": ">=18.18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-babel-transformer": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.82.5.tgz", - "integrity": "sha512-W/scFDnwJXSccJYnOFdGiYr9srhbHPdxX9TvvACOFsIXdLilh3XuxQl/wXW6jEJfgIb0jTvoTlwwrqvuwymr6Q==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.5.tgz", + "integrity": "sha512-2WbHILKMiJUzfdjmGOQOqU1bWi9//gqiclc/tkk/AIsrrVw3efhZ1uhkOwMTxUEPOzqoo091H0olLmVZH5FHGQ==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", - "hermes-parser": "0.29.1", + "hermes-parser": "0.35.0", + "metro-cache-key": "0.84.5", "nullthrows": "^1.1.1" }, "engines": { - "node": ">=18.18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz", + "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==", + "license": "MIT" + }, + "node_modules/metro-babel-transformer/node_modules/hermes-parser": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz", + "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.35.0" } }, "node_modules/metro-cache": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.82.5.tgz", - "integrity": "sha512-AwHV9607xZpedu1NQcjUkua8v7HfOTKfftl6Vc9OGr/jbpiJX6Gpy8E/V9jo/U9UuVYX2PqSUcVNZmu+LTm71Q==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.5.tgz", + "integrity": "sha512-WHS0n2OxQqtwEjSeQFPePNrMvEFhmQcUQM9cRJMHByWoi/GMWFBEWOf7hVkAM/0KRutAXNbDlSu/cZB6CyxgQQ==", "license": "MIT", "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", - "metro-core": "0.82.5" + "metro-core": "0.84.5" }, "engines": { - "node": ">=18.18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-cache-key": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.82.5.tgz", - "integrity": "sha512-qpVmPbDJuRLrT4kcGlUouyqLGssJnbTllVtvIgXfR7ZuzMKf0mGS+8WzcqzNK8+kCyakombQWR0uDd8qhWGJcA==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.5.tgz", + "integrity": "sha512-3dPB2TnvGjjf0/9O7AXVQURKXuQNauTZE7WpTGTlR017Gh/B5y0m/2wcqxfveUguHSpu89KhVxCAlr2k/H7uhQ==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" }, "engines": { - "node": ">=18.18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-config": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.82.5.tgz", - "integrity": "sha512-/r83VqE55l0WsBf8IhNmc/3z71y2zIPe5kRSuqA5tY/SL/ULzlHUJEMd1szztd0G45JozLwjvrhAzhDPJ/Qo/g==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.5.tgz", + "integrity": "sha512-zie+uN6oohscowi2S7ByU+wUw6CrT4ZxW9uAbONOObSxx86RGmnIAmjXHLkfmcdYoY7jzOPEbqcI6oeVmqyBQA==", "license": "MIT", "dependencies": { "connect": "^3.6.5", - "cosmiconfig": "^5.0.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", - "metro": "0.82.5", - "metro-cache": "0.82.5", - "metro-core": "0.82.5", - "metro-runtime": "0.82.5" - }, - "engines": { - "node": ">=18.18" - } - }, - "node_modules/metro-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/metro-config/node_modules/cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "license": "MIT", - "dependencies": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/metro-config/node_modules/import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", - "license": "MIT", - "dependencies": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" + "metro": "0.84.5", + "metro-cache": "0.84.5", + "metro-core": "0.84.5", + "metro-runtime": "0.84.5", + "yaml": "^2.6.1" }, "engines": { - "node": ">=4" - } - }, - "node_modules/metro-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/metro-config/node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "license": "MIT", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/metro-config/node_modules/resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", - "license": "MIT", - "engines": { - "node": ">=4" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-core": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.82.5.tgz", - "integrity": "sha512-OJL18VbSw2RgtBm1f2P3J5kb892LCVJqMvslXxuxjAPex8OH7Eb8RBfgEo7VZSjgb/LOf4jhC4UFk5l5tAOHHA==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.5.tgz", + "integrity": "sha512-xwm605hCi5Y6eJTTb8ZWo6pkUcoBEIyiQOfkZh5GwtDwUrP9SNhTQZhzJHrBCwwxlf3Ptl/pxWJgQ1rsNYMnrA==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", - "metro-resolver": "0.82.5" + "metro-resolver": "0.84.5" }, "engines": { - "node": ">=18.18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-file-map": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.82.5.tgz", - "integrity": "sha512-vpMDxkGIB+MTN8Af5hvSAanc6zXQipsAUO+XUx3PCQieKUfLwdoa8qaZ1WAQYRpaU+CJ8vhBcxtzzo3d9IsCIQ==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.5.tgz", + "integrity": "sha512-mlm/JL8toSbSc2akpKIGmzvrVRSCgZ5vkbycI34oMLoOnLGuLyC8WTyVJ6P0hZG/usDaGwZSl/s9BCRriqjGJA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -25978,66 +25448,65 @@ "walker": "^1.0.7" }, "engines": { - "node": ">=18.18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-minify-terser": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.82.5.tgz", - "integrity": "sha512-v6Nx7A4We6PqPu/ta1oGTqJ4Usz0P7c+3XNeBxW9kp8zayS3lHUKR0sY0wsCHInxZlNAEICx791x+uXytFUuwg==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.5.tgz", + "integrity": "sha512-BJoFwCEDsYnagPqarayInv2+diCDNDdLlaof/p6s9w4gh+gc9HXYM+pDvsKGKKUumpZswNF3Z/ftTMqKl/5IBg==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" }, "engines": { - "node": ">=18.18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-resolver": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.82.5.tgz", - "integrity": "sha512-kFowLnWACt3bEsuVsaRNgwplT8U7kETnaFHaZePlARz4Fg8tZtmRDUmjaD68CGAwc0rwdwNCkWizLYpnyVcs2g==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.5.tgz", + "integrity": "sha512-VSSnepg1k6LyCwtb6eirWdAWlpKwBG8Rdtsr1mU38rMelFyWgh3/QuMSiZIZAIjwg/fsa8GhW5/FO54CAUPCEA==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" }, "engines": { - "node": ">=18.18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-runtime": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.82.5.tgz", - "integrity": "sha512-rQZDoCUf7k4Broyw3Ixxlq5ieIPiR1ULONdpcYpbJQ6yQ5GGEyYjtkztGD+OhHlw81LCR2SUAoPvtTus2WDK5g==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.5.tgz", + "integrity": "sha512-U1m2+d1Pr+JO2/iVXBB2OfXXityz7tqwIorxfrT15IEgaHvpJBq/OHiqnOWPKJbUl3JcxjcdviZZOKk85oK4Qg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" }, "engines": { - "node": ">=18.18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-source-map": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.82.5.tgz", - "integrity": "sha512-wH+awTOQJVkbhn2SKyaw+0cd+RVSCZ3sHVgyqJFQXIee/yLs3dZqKjjeKKhhVeudgjXo7aE/vSu/zVfcQEcUfw==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.5.tgz", + "integrity": "sha512-2BtV5L9uPc49F13Gn5wiP6bX/EncqzqTIk2VL/0F/96Vo0YEOjluT/qktQjFODfqGFsucwnh5mPEAl/2jVEfeg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.3", - "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", - "@babel/types": "^7.25.2", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-symbolicate": "0.82.5", + "metro-symbolicate": "0.84.5", "nullthrows": "^1.1.1", - "ob1": "0.82.5", + "ob1": "0.84.5", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "engines": { - "node": ">=18.18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-source-map/node_modules/source-map": { @@ -26050,14 +25519,14 @@ } }, "node_modules/metro-symbolicate": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.82.5.tgz", - "integrity": "sha512-1u+07gzrvYDJ/oNXuOG1EXSvXZka/0JSW1q2EYBWerVKMOhvv9JzDGyzmuV7hHbF2Hg3T3S2uiM36sLz1qKsiw==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.5.tgz", + "integrity": "sha512-rQ40zYDAkaWBN9yvjUuAD0ZpzBMZSoKyGYXnb5JrfbKjun7fTvfoLHL3KXFYenBTYZkQtlp4cKSCv/1utxFyOw==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-source-map": "0.82.5", + "metro-source-map": "0.84.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" @@ -26066,7 +25535,7 @@ "metro-symbolicate": "src/index.js" }, "engines": { - "node": ">=18.18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-symbolicate/node_modules/source-map": { @@ -26079,44 +25548,57 @@ } }, "node_modules/metro-transform-plugins": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.82.5.tgz", - "integrity": "sha512-57Bqf3rgq9nPqLrT2d9kf/2WVieTFqsQ6qWHpEng5naIUtc/Iiw9+0bfLLWSAw0GH40iJ4yMjFcFJDtNSYynMA==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.5.tgz", + "integrity": "sha512-+InaSVGaOyt0DyRo4Y/zIdPI6CZwnbNho5LAL23tgmuGwv7fyfkF7kKfPjZcfxXBcoYdTLLFnCfCH/dHSiCqNg==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", - "@babel/generator": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.3", + "@babel/generator": "^7.29.1", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" }, "engines": { - "node": ">=18.18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/metro-transform-worker": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.82.5.tgz", - "integrity": "sha512-mx0grhAX7xe+XUQH6qoHHlWedI8fhSpDGsfga7CpkO9Lk9W+aPitNtJWNGrW8PfjKEWbT9Uz9O50dkI8bJqigw==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.5.tgz", + "integrity": "sha512-ui1Z8x4s5RL36gMmKLaMMO7O9NNDHNdthEZSCDQHAau3JcAsTaFOK6I+2q4I/kW5u8hSEjJk9L45TXSVJw6g1A==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", - "@babel/types": "^7.25.2", + "@babel/generator": "^7.29.1", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", - "metro": "0.82.5", - "metro-babel-transformer": "0.82.5", - "metro-cache": "0.82.5", - "metro-cache-key": "0.82.5", - "metro-minify-terser": "0.82.5", - "metro-source-map": "0.82.5", - "metro-transform-plugins": "0.82.5", + "metro": "0.84.5", + "metro-babel-transformer": "0.84.5", + "metro-cache": "0.84.5", + "metro-cache-key": "0.84.5", + "metro-minify-terser": "0.84.5", + "metro-source-map": "0.84.5", + "metro-transform-plugins": "0.84.5", "nullthrows": "^1.1.1" }, "engines": { - "node": ">=18.18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/metro/node_modules/ci-info": { @@ -26125,6 +25607,53 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "license": "MIT" }, + "node_modules/metro/node_modules/hermes-estree": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz", + "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==", + "license": "MIT" + }, + "node_modules/metro/node_modules/hermes-parser": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz", + "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.35.0" + } + }, + "node_modules/metro/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/metro/node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/metro/node_modules/serialize-error": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", @@ -26144,9 +25673,9 @@ } }, "node_modules/metro/node_modules/ws": { - "version": "7.5.11", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", - "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", "license": "MIT", "engines": { "node": ">=8.3.0" @@ -26279,16 +25808,6 @@ "dom-walk": "^0.1.0" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -29730,15 +29249,15 @@ } }, "node_modules/ob1": { - "version": "0.82.5", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.82.5.tgz", - "integrity": "sha512-QyQQ6e66f+Ut/qUVjEce0E/wux5nAGLXYZDn1jr15JWstHsCH3l6VVrg8NKDptW9NEiBXKOJeGF/ydxeSDF3IQ==", + "version": "0.84.5", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.5.tgz", + "integrity": "sha512-aH9RkoZc7w/90HBamFxTw8ZLFr05wXS+iOnvmrgo53Ep8Pyrm5FieQSaPIVROkfFVQISeD/zo92fes26TOwe+A==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" }, "engines": { - "node": ">=18.18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, "node_modules/object-assign": { @@ -31245,12 +30764,6 @@ "integrity": "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==", "license": "MIT" }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -31530,15 +31043,6 @@ "dev": true, "license": "MIT" }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -31682,6 +31186,22 @@ } } }, + "node_modules/react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-freeze": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", @@ -31744,59 +31264,59 @@ "license": "MIT" }, "node_modules/react-native": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.79.7.tgz", - "integrity": "sha512-7B2FJt/P+qulrkjWNttofiQjpZ5czSnL00kr6kQ9GpiykF/agX6Z2GVX6e5ggpQq2jqtyLvRtHIiUnKPYM77+w==", + "version": "0.85.2", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.85.2.tgz", + "integrity": "sha512-GFWEPwLYirfj5X8gMtXOWtqX0cqUEURRHETZfFk37VCa4++izrKvGvv24anvuyulXV87NAhVkfNw93rLg3HByw==", "license": "MIT", "dependencies": { - "@jest/create-cache-key-function": "^29.7.0", - "@react-native/assets-registry": "0.79.7", - "@react-native/codegen": "0.79.7", - "@react-native/community-cli-plugin": "0.79.7", - "@react-native/gradle-plugin": "0.79.7", - "@react-native/js-polyfills": "0.79.7", - "@react-native/normalize-colors": "0.79.7", - "@react-native/virtualized-lists": "0.79.7", + "@react-native/assets-registry": "0.85.2", + "@react-native/codegen": "0.85.2", + "@react-native/community-cli-plugin": "0.85.2", + "@react-native/gradle-plugin": "0.85.2", + "@react-native/js-polyfills": "0.85.2", + "@react-native/normalize-colors": "0.85.2", + "@react-native/virtualized-lists": "0.85.2", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", - "babel-jest": "^29.7.0", - "babel-plugin-syntax-hermes-parser": "0.25.1", + "babel-plugin-syntax-hermes-parser": "0.33.3", "base64-js": "^1.5.1", - "chalk": "^4.0.0", "commander": "^12.0.0", - "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.6", - "glob": "^7.1.1", + "hermes-compiler": "250829098.0.10", "invariant": "^2.2.4", - "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", - "metro-runtime": "^0.82.0", - "metro-source-map": "^0.82.0", + "metro-runtime": "^0.84.0", + "metro-source-map": "^0.84.0", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", - "react-devtools-core": "^6.1.1", + "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", - "scheduler": "0.25.0", + "scheduler": "0.27.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", + "tinyglobby": "^0.2.15", "whatwg-fetch": "^3.0.0", - "ws": "^6.2.3", + "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "react-native": "cli.js" }, "engines": { - "node": ">=18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" }, "peerDependencies": { - "@types/react": "^19.0.0", - "react": "^19.0.0" + "@react-native/jest-preset": "0.85.2", + "@types/react": "^19.1.1", + "react": "^19.2.3" }, "peerDependenciesMeta": { + "@react-native/jest-preset": { + "optional": true + }, "@types/react": { "optional": true } @@ -32123,51 +31643,32 @@ } }, "node_modules/react-native/node_modules/@react-native/codegen": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.79.7.tgz", - "integrity": "sha512-uOjsqpLccl0+8iHPBmrkFrWwK0ctW28M83Ln2z43HRNubkxk5Nxd3DoyphFPL/BwTG79Ixu+BqpCS7b9mtizpw==", + "version": "0.85.2", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.85.2.tgz", + "integrity": "sha512-XCginmxh0//++EXVOEJHBVZxHla294FzLCFF6jXwAUjvXVhqyIKyxhABfz+r4OOmaiuWk4Rtd4arqdAzeHeprg==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", - "@babel/parser": "^7.25.3", - "glob": "^7.1.1", - "hermes-parser": "0.25.1", + "@babel/parser": "^7.29.0", + "hermes-parser": "0.33.3", "invariant": "^2.2.4", "nullthrows": "^1.1.1", + "tinyglobby": "^0.2.15", "yargs": "^17.6.2" }, "engines": { - "node": ">=18" + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" }, "peerDependencies": { "@babel/core": "*" } }, - "node_modules/react-native/node_modules/@react-native/js-polyfills": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.79.7.tgz", - "integrity": "sha512-Djgvfz6AOa8ZEWyv+KA/UnP+ZruM+clCauFTR6NeRyD8YELvXGt+6A231SwpNdRkM7aTDMv0cM0NUbAMEPy+1A==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/react-native/node_modules/@react-native/normalize-colors": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.7.tgz", - "integrity": "sha512-RrvewhdanEWhlyrHNWGXGZCc6MY0JGpNgRzA8y6OomDz0JmlnlIsbBHbNpPnIrt9Jh2KaV10KTscD1Ry8xU9gQ==", + "version": "0.85.2", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.85.2.tgz", + "integrity": "sha512-svuOLtjbFGXDdHsriHXuND5FgHg7XlkOXCbH/8+X4t76YLH6qSTffSIQQrKLDL5mn4EFU+Oh/PNO0/FfpnTOTg==", "license": "MIT" }, - "node_modules/react-native/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/react-native/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -32177,38 +31678,11 @@ "node": ">=18" } }, - "node_modules/react-native/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/react-native/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } + "node_modules/react-native/node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, "node_modules/react-native/node_modules/semver": { "version": "7.8.1", @@ -32223,12 +31697,24 @@ } }, "node_modules/react-native/node_modules/ws": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.4.tgz", - "integrity": "sha512-PNIUUyLI5YpkJZj60YBzX1o0ByQ4ovvfmq9N/Kig/PAYbVlGyz4R6G0SEWrD0O9acc0sT2+IdMBVLFv8FSi0Nw==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", "license": "MIT", - "dependencies": { - "async-limiter": "~1.0.0" + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/react-refresh": { @@ -32539,33 +32025,6 @@ "node": ">= 12.13.0" } }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/redent/node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/reduce-flatten": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", @@ -33269,6 +32728,7 @@ "version": "0.25.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "dev": true, "license": "MIT" }, "node_modules/scrypt-js": { @@ -34785,6 +34245,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/sshpk": { @@ -34849,6 +34310,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" @@ -34861,6 +34323,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -35901,9 +35364,9 @@ } }, "node_modules/terser": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", - "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "version": "5.51.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.0.tgz", + "integrity": "sha512-myiQ6aFnxDOjdiXdTlC8ngVccQD88uHXTx5RUQOSEnarDYgJMjDagwAQszcOusjvmf4YqWsxoD1+MY+oAJRmpw==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -35928,6 +35391,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", @@ -35942,6 +35406,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -35953,6 +35418,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -35973,6 +35439,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -36129,7 +35596,6 @@ "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -36146,7 +35612,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -36618,6 +36083,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -38910,6 +38376,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -39133,7 +38600,6 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/scripts/generate-sdks.js b/scripts/generate-sdks.js index 47f0d5f5..16acf851 100644 --- a/scripts/generate-sdks.js +++ b/scripts/generate-sdks.js @@ -4,23 +4,57 @@ const fs = require('fs'); const path = require('path'); const root = process.cwd(); -const openApiPath = path.join(root, 'spec/openapi.yaml'); +const jsonPath = path.join(root, 'developer-portal/docs/openapi.json'); +const yamlPath = path.join(root, 'spec/openapi.yaml'); const outputDir = path.join(root, 'sdks/generated'); const outputPath = path.join(outputDir, 'endpoints.json'); -const spec = fs.readFileSync(openApiPath, 'utf8'); -const endpoints = [...spec.matchAll(/^ \/(.+):$/gm)].map((match) => { - const pathName = `/${match[1]}`; - const methodName = match[1].replace(/_([a-z])/g, (_, char) => char.toUpperCase()); - return { - path: pathName, - method: 'POST', - operation: methodName, - }; -}); +let specObj = null; +let specSource = ''; + +if (fs.existsSync(jsonPath)) { + const content = fs.readFileSync(jsonPath, 'utf8'); + specObj = JSON.parse(content); + specSource = 'developer-portal/docs/openapi.json'; +} + +const endpoints = []; + +if (specObj && specObj.paths) { + for (const [pathKey, pathItem] of Object.entries(specObj.paths)) { + const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head']; + for (const method of httpMethods) { + if (pathItem[method]) { + const op = pathItem[method]; + const operationId = + op.operationId || + `${method}${pathKey.replace(/[^a-zA-Z0-9]/g, '_').replace(/_+/g, '_')}`; + endpoints.push({ + path: pathKey, + method: method.toUpperCase(), + operation: operationId, + summary: op.summary || '', + }); + } + } + } +} else if (fs.existsSync(yamlPath)) { + const spec = fs.readFileSync(yamlPath, 'utf8'); + specSource = 'spec/openapi.yaml'; + const matches = [...spec.matchAll(/^ \/(.+):$/gm)]; + for (const match of matches) { + const pathName = `/${match[1]}`; + const methodName = match[1].replace(/_([a-z])/g, (_, char) => char.toUpperCase()); + endpoints.push({ + path: pathName, + method: 'POST', + operation: methodName, + }); + } +} if (!endpoints.length) { - throw new Error(`No endpoints found in ${openApiPath}`); + throw new Error(`No endpoints found in OpenAPI specifications`); } fs.mkdirSync(outputDir, { recursive: true }); @@ -28,7 +62,8 @@ fs.writeFileSync( outputPath, `${JSON.stringify( { - source: 'spec/openapi.yaml', + openapiVersion: specObj ? specObj.openapi : '3.1.0', + source: specSource, generatedBy: 'scripts/generate-sdks.js', endpoints, }, diff --git a/sdks/generated/endpoints.json b/sdks/generated/endpoints.json index 06ba07de..fffbcb3e 100644 --- a/sdks/generated/endpoints.json +++ b/sdks/generated/endpoints.json @@ -1,40 +1,25 @@ { - "source": "docs/openapi.yaml", + "openapiVersion": "3.1.0", + "source": "developer-portal/docs/openapi.json", "generatedBy": "scripts/generate-sdks.js", "endpoints": [ - { "path": "/initialize", "method": "POST", "operation": "initialize" }, - { "path": "/create_plan", "method": "POST", "operation": "createPlan" }, - { "path": "/deactivate_plan", "method": "POST", "operation": "deactivatePlan" }, - { "path": "/subscribe", "method": "POST", "operation": "subscribe" }, - { "path": "/cancel_subscription", "method": "POST", "operation": "cancelSubscription" }, - { "path": "/pause_subscription", "method": "POST", "operation": "pauseSubscription" }, - { "path": "/resume_subscription", "method": "POST", "operation": "resumeSubscription" }, - { "path": "/charge_subscription", "method": "POST", "operation": "chargeSubscription" }, - { "path": "/request_refund", "method": "POST", "operation": "requestRefund" }, - { "path": "/approve_refund", "method": "POST", "operation": "approveRefund" }, - { "path": "/reject_refund", "method": "POST", "operation": "rejectRefund" }, - { "path": "/get_plan", "method": "POST", "operation": "getPlan" }, - { "path": "/get_subscription", "method": "POST", "operation": "getSubscription" }, - { "path": "/get_user_subscriptions", "method": "POST", "operation": "getUserSubscriptions" }, - { "path": "/get_merchant_plans", "method": "POST", "operation": "getMerchantPlans" }, - { "path": "/get_plan_count", "method": "POST", "operation": "getPlanCount" }, - { "path": "/get_subscription_count", "method": "POST", "operation": "getSubscriptionCount" }, - { "path": "/v1/subscriptions", "method": "GET", "operation": "listSubscriptions" }, - { "path": "/v1/subscriptions", "method": "POST", "operation": "createSubscription" }, - { "path": "/v1/subscriptions/{id}", "method": "PATCH", "operation": "updateSubscription" }, - { "path": "/v1/dunning", "method": "GET", "operation": "listDunningEntries" }, - { "path": "/v1/dunning", "method": "POST", "operation": "createDunningEntry" }, - { "path": "/v1/dunning/{id}", "method": "GET", "operation": "getDunningEntry" }, - { "path": "/v1/dunning/{id}/pause", "method": "POST", "operation": "pauseDunning" }, - { "path": "/v1/dunning/{id}/resolve", "method": "POST", "operation": "resolveDunning" }, - { "path": "/v1/billing/invoices", "method": "GET", "operation": "listInvoices" }, - { "path": "/v1/billing/invoices/{id}", "method": "GET", "operation": "getInvoice" }, - { "path": "/v1/billing/history", "method": "GET", "operation": "listBillingHistory" }, - { "path": "/v1/usage", "method": "POST", "operation": "ingestUsage" }, - { "path": "/v1/usage", "method": "GET", "operation": "listUsageRecords" }, - { "path": "/v1/usage/summary", "method": "GET", "operation": "getUsageSummary" }, - { "path": "/v1/webhooks", "method": "GET", "operation": "listWebhooks" }, - { "path": "/v1/webhooks", "method": "POST", "operation": "createWebhook" }, - { "path": "/v1/webhooks/{id}", "method": "DELETE", "operation": "deleteWebhook" } + { + "path": "/auth/login", + "method": "POST", + "operation": "loginUser", + "summary": "Authenticate user" + }, + { + "path": "/subscriptions", + "method": "GET", + "operation": "listSubscriptions", + "summary": "List subscriptions" + }, + { + "path": "/subscriptions", + "method": "POST", + "operation": "createSubscription", + "summary": "Create subscription" + } ] } diff --git a/spec/openapi.yaml b/spec/openapi.yaml new file mode 100644 index 00000000..fe361bee --- /dev/null +++ b/spec/openapi.yaml @@ -0,0 +1,72 @@ +openapi: 3.1.0 +info: + title: SubTrackr API + version: 1.0.0 + description: API specification for SubTrackr subscription management, notifications, and payments. + contact: + name: SubTrackr Support + email: support@subtrackr.io +servers: + - url: https://api.subtrackr.io/v1 + description: Production Server + - url: http://localhost:3000/v1 + description: Local Development Server +paths: + /auth/login: + post: + summary: Authenticate user + operationId: loginUser + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + format: email + password: + type: string + required: + - email + - password + responses: + '200': + description: Authentication successful + content: + application/json: + schema: + type: object + properties: + token: + type: string + expiresIn: + type: integer + /subscriptions: + get: + summary: List subscriptions + operationId: listSubscriptions + security: + - BearerAuth: [] + responses: + '200': + description: List of active user subscriptions + post: + summary: Create subscription + operationId: createSubscription + security: + - BearerAuth: [] + responses: + '201': + description: Subscription created +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key diff --git a/src/context/__tests__/SubscriptionContext.test.tsx b/src/context/__tests__/SubscriptionContext.test.tsx new file mode 100644 index 00000000..26c1f968 --- /dev/null +++ b/src/context/__tests__/SubscriptionContext.test.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { + SubscriptionProvider, + useSubscriptionContext, + useSubscriptions, + useSubscriptionStats, + useSubscriptionStatus, +} from '../SubscriptionContext'; +import { SubscriptionCategory, BillingCycle } from '../../types/subscription'; + +const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} +); + +describe('SubscriptionContext', () => { + it('should provide subscription context values', () => { + const { result } = renderHook(() => useSubscriptionContext(), { wrapper }); + + expect(result.current.subscriptions).toBeDefined(); + expect(result.current.stats).toBeDefined(); + expect(typeof result.current.addSubscription).toBe('function'); + expect(typeof result.current.getActiveSubscriptions).toBe('function'); + }); + + it('should filter subscriptions using useSubscriptions hook', () => { + const { result } = renderHook( + () => + useSubscriptions({ + category: SubscriptionCategory.ENTERTAINMENT, + billingCycle: BillingCycle.MONTHLY, + }), + { wrapper } + ); + + expect(Array.isArray(result.current)).toBe(true); + }); + + it('should provide stats and status hooks', () => { + const { result: statsResult } = renderHook(() => useSubscriptionStats(), { wrapper }); + expect(statsResult.current).toBeDefined(); + + const { result: statusResult } = renderHook(() => useSubscriptionStatus(), { wrapper }); + expect(statusResult.current.isLoading).toBeDefined(); + }); +}); diff --git a/src/contracts/types/ERC20.ts b/src/contracts/types/ERC20.ts index 855f15ef..c6b0574b 100644 --- a/src/contracts/types/ERC20.ts +++ b/src/contracts/types/ERC20.ts @@ -3,32 +3,35 @@ /* eslint-disable */ import type { BaseContract, + BigNumber, BytesLike, - FunctionFragment, - Result, - Interface, - AddressLike, - ContractRunner, - ContractMethod, - Listener, + CallOverrides, + PopulatedTransaction, + Signer, + utils, } from "ethers"; +import type { FunctionFragment, Result } from "@ethersproject/abi"; +import type { Listener, Provider } from "@ethersproject/providers"; import type { - TypedContractEvent, - TypedDeferredTopicFilter, - TypedEventLog, + TypedEventFilter, + TypedEvent, TypedListener, - TypedContractMethod, + OnEvent, } from "./common"; -export interface ERC20Interface extends Interface { +export interface ERC20Interface extends utils.Interface { + functions: { + "balanceOf(address)": FunctionFragment; + "decimals()": FunctionFragment; + "name()": FunctionFragment; + "symbol()": FunctionFragment; + }; + getFunction( - nameOrSignature: "balanceOf" | "decimals" | "name" | "symbol" + nameOrSignatureOrTopic: "balanceOf" | "decimals" | "name" | "symbol" ): FunctionFragment; - encodeFunctionData( - functionFragment: "balanceOf", - values: [AddressLike] - ): string; + encodeFunctionData(functionFragment: "balanceOf", values: [string]): string; encodeFunctionData(functionFragment: "decimals", values?: undefined): string; encodeFunctionData(functionFragment: "name", values?: undefined): string; encodeFunctionData(functionFragment: "symbol", values?: undefined): string; @@ -37,75 +40,86 @@ export interface ERC20Interface extends Interface { decodeFunctionResult(functionFragment: "decimals", data: BytesLike): Result; decodeFunctionResult(functionFragment: "name", data: BytesLike): Result; decodeFunctionResult(functionFragment: "symbol", data: BytesLike): Result; + + events: {}; } export interface ERC20 extends BaseContract { - connect(runner?: ContractRunner | null): ERC20; - waitForDeployment(): Promise; + connect(signerOrProvider: Signer | Provider | string): this; + attach(addressOrName: string): this; + deployed(): Promise; interface: ERC20Interface; - queryFilter( - event: TCEvent, - fromBlockOrBlockhash?: string | number | undefined, - toBlock?: string | number | undefined - ): Promise>>; - queryFilter( - filter: TypedDeferredTopicFilter, + queryFilter( + event: TypedEventFilter, fromBlockOrBlockhash?: string | number | undefined, toBlock?: string | number | undefined - ): Promise>>; - - on( - event: TCEvent, - listener: TypedListener - ): Promise; - on( - filter: TypedDeferredTopicFilter, - listener: TypedListener - ): Promise; - - once( - event: TCEvent, - listener: TypedListener - ): Promise; - once( - filter: TypedDeferredTopicFilter, - listener: TypedListener - ): Promise; - - listeners( - event: TCEvent - ): Promise>>; - listeners(eventName?: string): Promise>; - removeAllListeners( - event?: TCEvent - ): Promise; - - balanceOf: TypedContractMethod<[account: AddressLike], [bigint], "view">; - - decimals: TypedContractMethod<[], [bigint], "view">; - - name: TypedContractMethod<[], [string], "view">; - - symbol: TypedContractMethod<[], [string], "view">; - - getFunction( - key: string | FunctionFragment - ): T; + ): Promise>; - getFunction( - nameOrSignature: "balanceOf" - ): TypedContractMethod<[account: AddressLike], [bigint], "view">; - getFunction( - nameOrSignature: "decimals" - ): TypedContractMethod<[], [bigint], "view">; - getFunction( - nameOrSignature: "name" - ): TypedContractMethod<[], [string], "view">; - getFunction( - nameOrSignature: "symbol" - ): TypedContractMethod<[], [string], "view">; + listeners( + eventFilter?: TypedEventFilter + ): Array>; + listeners(eventName?: string): Array; + removeAllListeners( + eventFilter: TypedEventFilter + ): this; + removeAllListeners(eventName?: string): this; + off: OnEvent; + on: OnEvent; + once: OnEvent; + removeListener: OnEvent; + + functions: { + balanceOf(account: string, overrides?: CallOverrides): Promise<[BigNumber]>; + + decimals(overrides?: CallOverrides): Promise<[number]>; + + name(overrides?: CallOverrides): Promise<[string]>; + + symbol(overrides?: CallOverrides): Promise<[string]>; + }; + + balanceOf(account: string, overrides?: CallOverrides): Promise; + + decimals(overrides?: CallOverrides): Promise; + + name(overrides?: CallOverrides): Promise; + + symbol(overrides?: CallOverrides): Promise; + + callStatic: { + balanceOf(account: string, overrides?: CallOverrides): Promise; + + decimals(overrides?: CallOverrides): Promise; + + name(overrides?: CallOverrides): Promise; + + symbol(overrides?: CallOverrides): Promise; + }; filters: {}; + + estimateGas: { + balanceOf(account: string, overrides?: CallOverrides): Promise; + + decimals(overrides?: CallOverrides): Promise; + + name(overrides?: CallOverrides): Promise; + + symbol(overrides?: CallOverrides): Promise; + }; + + populateTransaction: { + balanceOf( + account: string, + overrides?: CallOverrides + ): Promise; + + decimals(overrides?: CallOverrides): Promise; + + name(overrides?: CallOverrides): Promise; + + symbol(overrides?: CallOverrides): Promise; + }; } diff --git a/src/contracts/types/common.ts b/src/contracts/types/common.ts index 56b5f21e..2fc40c7f 100644 --- a/src/contracts/types/common.ts +++ b/src/contracts/types/common.ts @@ -1,65 +1,32 @@ /* Autogenerated file. Do not edit manually. */ /* tslint:disable */ /* eslint-disable */ -import type { - FunctionFragment, - Typed, - EventFragment, - ContractTransaction, - ContractTransactionResponse, - DeferredTopicFilter, - EventLog, - TransactionRequest, - LogDescription, -} from "ethers"; - -export interface TypedDeferredTopicFilter<_TCEvent extends TypedContractEvent> - extends DeferredTopicFilter {} - -export interface TypedContractEvent< - InputTuple extends Array = any, - OutputTuple extends Array = any, - OutputObject = any -> { - (...args: Partial): TypedDeferredTopicFilter< - TypedContractEvent - >; - name: string; - fragment: EventFragment; - getFragment(...args: Partial): EventFragment; +import type { Listener } from "@ethersproject/providers"; +import type { Event, EventFilter } from "ethers"; + +export interface TypedEvent< + TArgsArray extends Array = any, + TArgsObject = any +> extends Event { + args: TArgsArray & TArgsObject; } -type __TypechainAOutputTuple = T extends TypedContractEvent< - infer _U, - infer W -> - ? W - : never; -type __TypechainOutputObject = T extends TypedContractEvent< - infer _U, - infer _W, - infer V -> - ? V - : never; +export interface TypedEventFilter<_TEvent extends TypedEvent> + extends EventFilter {} -export interface TypedEventLog - extends Omit { - args: __TypechainAOutputTuple & __TypechainOutputObject; +export interface TypedListener { + (...listenerArg: [...__TypechainArgsArray, TEvent]): void; } -export interface TypedLogDescription - extends Omit { - args: __TypechainAOutputTuple & __TypechainOutputObject; -} +type __TypechainArgsArray = T extends TypedEvent ? U : never; -export type TypedListener = ( - ...listenerArg: [ - ...__TypechainAOutputTuple, - TypedEventLog, - ...undefined[] - ] -) => void; +export interface OnEvent { + ( + eventFilter: TypedEventFilter, + listener: TypedListener + ): TRes; + (eventName: string, listener: Listener): TRes; +} export type MinEthersFactory = { deploy(...a: ARGS[]): Promise; @@ -71,61 +38,7 @@ export type GetContractTypeFromFactory = F extends MinEthersFactory< > ? C : never; + export type GetARGsTypeFromFactory = F extends MinEthersFactory ? Parameters : never; - -export type StateMutability = "nonpayable" | "payable" | "view"; - -export type BaseOverrides = Omit; -export type NonPayableOverrides = Omit< - BaseOverrides, - "value" | "blockTag" | "enableCcipRead" ->; -export type PayableOverrides = Omit< - BaseOverrides, - "blockTag" | "enableCcipRead" ->; -export type ViewOverrides = Omit; -export type Overrides = S extends "nonpayable" - ? NonPayableOverrides - : S extends "payable" - ? PayableOverrides - : ViewOverrides; - -export type PostfixOverrides, S extends StateMutability> = - | A - | [...A, Overrides]; -export type ContractMethodArgs< - A extends Array, - S extends StateMutability -> = PostfixOverrides<{ [I in keyof A]-?: A[I] | Typed }, S>; - -export type DefaultReturnType = R extends Array ? R[0] : R; - -// export interface ContractMethod = Array, R = any, D extends R | ContractTransactionResponse = R | ContractTransactionResponse> { -export interface TypedContractMethod< - A extends Array = Array, - R = any, - S extends StateMutability = "payable" -> { - (...args: ContractMethodArgs): S extends "view" - ? Promise> - : Promise; - - name: string; - - fragment: FunctionFragment; - - getFragment(...args: ContractMethodArgs): FunctionFragment; - - populateTransaction( - ...args: ContractMethodArgs - ): Promise; - staticCall( - ...args: ContractMethodArgs - ): Promise>; - send(...args: ContractMethodArgs): Promise; - estimateGas(...args: ContractMethodArgs): Promise; - staticCallResult(...args: ContractMethodArgs): Promise; -} diff --git a/src/contracts/types/factories/ERC20__factory.ts b/src/contracts/types/factories/ERC20__factory.ts index 85bf3612..68b4c7a6 100644 --- a/src/contracts/types/factories/ERC20__factory.ts +++ b/src/contracts/types/factories/ERC20__factory.ts @@ -2,7 +2,8 @@ /* tslint:disable */ /* eslint-disable */ -import { Contract, Interface, type ContractRunner } from "ethers"; +import { Contract, Signer, utils } from "ethers"; +import type { Provider } from "@ethersproject/providers"; import type { ERC20, ERC20Interface } from "../ERC20"; const _abi = [ @@ -72,9 +73,9 @@ const _abi = [ export class ERC20__factory { static readonly abi = _abi; static createInterface(): ERC20Interface { - return new Interface(_abi) as ERC20Interface; + return new utils.Interface(_abi) as ERC20Interface; } - static connect(address: string, runner?: ContractRunner | null): ERC20 { - return new Contract(address, _abi, runner) as unknown as ERC20; + static connect(address: string, signerOrProvider: Signer | Provider): ERC20 { + return new Contract(address, _abi, signerOrProvider) as ERC20; } } diff --git a/src/errors/index.ts b/src/errors/index.ts index c50482d9..c26f78fb 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -255,3 +255,21 @@ export class WebSocketError extends AppError { Object.setPrototypeOf(this, new.target.prototype); } } + +export function isAppError(error: unknown): error is AppError { + return error instanceof AppError; +} + +export function fromUnknownAppError( + error: unknown, + defaultMessage = 'An unexpected error occurred', + requestId?: string +): AppError { + if (isAppError(error)) { + return error; + } + if (error instanceof Error) { + return new AppError('UNKNOWN_ERROR', error.message, undefined, error, undefined, requestId); + } + return new AppError('UNKNOWN_ERROR', defaultMessage, undefined, error, undefined, requestId); +} diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index c0efc320..6098d998 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -5,369 +5,39 @@ import { navigationRef } from './navigationRef'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { useTranslation } from 'react-i18next'; -import { lazyScreen, prefetchModule } from '../utils/lazyLoading'; +import { prefetchModule } from '../utils/lazyLoading'; import { RootStackParamList, TabParamList } from './types'; import { useTheme } from '../theme'; import { darkNavigationTheme, lightNavigationTheme } from '../theme/navigationTheme'; +import { NavigationErrorBoundary } from './NavigationErrorBoundary'; -// Eagerly loaded primary entrypoints for instant rendering -import HomeScreen from '../screens/HomeScreen'; -import { SettingsScreen } from '../screens/SettingsScreen'; - -// Lazy loaded auxiliary and heavy screens with suspense/retry support -const AddSubscriptionScreen = lazyScreen(() => import('../screens/AddSubscriptionScreen')); -const CancellationFlowScreen = lazyScreen(() => import('../screens/CancellationFlowScreen')); -const WalletConnectScreen = lazyScreen(() => import('../screens/WalletConnectV2Screen')); -const CryptoPaymentScreen = lazyScreen(() => import('../screens/CryptoPaymentScreen')); -const CommunityScreen = lazyScreen(() => import('../screens/CommunityScreen')); -const ProfileScreen = lazyScreen(() => import('../screens/ProfileScreen')); -const SubscriptionDetailScreen = lazyScreen(() => import('../screens/SubscriptionDetailScreen')); -const InvoiceListScreen = lazyScreen(() => import('../screens/InvoiceListScreen')); -const InvoiceDetailScreen = lazyScreen(() => import('../screens/InvoiceDetailScreen')); -const AnalyticsScreen = lazyScreen(() => import('../screens/AnalyticsScreen')); -const SlaDashboard = lazyScreen(() => import('../screens/SlaDashboard')); -const GDPRSettingsScreen = lazyScreen(() => import('../screens/GDPRSettingsScreen')); -const LanguageSettingsScreen = lazyScreen(() => import('../screens/LanguageSettingsScreen')); -const SessionManagementScreen = lazyScreen(() => import('../screens/SessionManagementScreen')); -const CalendarIntegrationScreen = lazyScreen(() => import('../screens/CalendarIntegrationScreen')); -const AccountingExportScreen = lazyScreen(() => import('../screens/AccountingExportScreen')); -const WebhookSettingsScreen = lazyScreen(() => import('../screens/WebhookSettingsScreen')); -const ErrorDashboardScreen = lazyScreen(() => import('../screens/ErrorDashboardScreen')); -const ImportScreen = lazyScreen(() => import('../screens/ImportScreen')); -const ExportScreen = lazyScreen(() => import('../screens/ExportScreen')); -const BatchOperationsScreen = lazyScreen(() => - import('../../app/screens/BatchOperationsScreen').then((m) => ({ - default: m.BatchOperationsScreen, - })) -); -const AdminDashboardScreen = lazyScreen(() => import('../screens/AdminDashboardScreen')); -const FraudDashboard = lazyScreen(() => import('../screens/FraudDashboard')); -const GroupManagementScreen = lazyScreen(() => import('../screens/GroupManagementScreen')); -const TaxSettingsScreen = lazyScreen(() => import('../screens/TaxSettingsScreen')); -const SupportDashboardScreen = lazyScreen(() => import('../screens/SupportDashboardScreen')); -const SegmentManagementScreen = lazyScreen(() => - import('../screens/SegmentManagementScreen').then((m) => ({ default: m.SegmentManagementScreen })) -); -const SegmentDetailScreen = lazyScreen(() => - import('../screens/SegmentDetailScreen').then((m) => ({ default: m.SegmentDetailScreen })) -); -const GamificationScreen = lazyScreen(() => - import('../screens/GamificationScreen').then((m) => ({ default: m.GamificationScreen })) -); -const RevenueReportScreen = lazyScreen(() => import('../screens/RevenueReportScreen')); -const UsageDashboardScreen = lazyScreen(() => import('../screens/UsageDashboard')); -const MerchantOnboardingScreen = lazyScreen(() => import('../screens/MerchantOnboardingScreen')); -const AffiliateDashboardScreen = lazyScreen(() => import('../screens/AffiliateDashboardScreen')); -const LoyaltyDashboardScreen = lazyScreen(() => import('../screens/LoyaltyDashboardScreen')); -const CampaignManagementScreen = lazyScreen(() => import('../screens/CampaignManagementScreen')); -const DeveloperPortalScreen = lazyScreen(() => import('../screens/DeveloperPortalScreen')); -const SandboxDashboardScreen = lazyScreen(() => import('../screens/SandboxDashboardScreen')); -const ApiKeyManagementScreen = lazyScreen(() => import('../screens/ApiKeyManagementScreen')); -const DocumentationPortalScreen = lazyScreen(() => import('../screens/DocumentationPortalScreen')); -const IntegrationGuidesScreen = lazyScreen(() => import('../screens/IntegrationGuidesScreen')); -const PerformanceDashboardScreen = lazyScreen( - () => import('../screens/PerformanceDashboardScreen') -); -const EditSubscriptionScreen = lazyScreen(() => import('../screens/EditSubscriptionScreen')); -const ChangePlanScreen = lazyScreen(() => import('../screens/ChangePlanScreen')); -const BillingSettingsScreen = lazyScreen(() => import('../screens/BillingSettingsScreen')); -const PaymentMethodsScreen = lazyScreen(() => - import('../../app/screens/PaymentMethodsScreen').then((m) => ({ - default: m.PaymentMethodsScreen, - })) -); -const AnalyticsDashboard = lazyScreen(() => import('../../app/screens/AnalyticsDashboard')); -const AdvancedSearchScreen = lazyScreen(() => - import('../../app/screens/AdvancedSearchScreen').then((m) => ({ - default: m.AdvancedSearchScreen, - })) -); +// Import feature-based stack modules +import { + SubscriptionStack, + AnalyticsStack, + SettingsStack, + WalletStack, +} from './modules'; const Tab = createBottomTabNavigator(); const Stack = createNativeStackNavigator(); -const HomeStack = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -const SettingsStack = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/** + * Root Stack Navigator encapsulating tab navigation and global modal screens. + */ +const RootNavigator = () => ( + + + + + + ); +/** + * Modular Bottom Tab Navigator organizing main entry points. + */ const TabNavigator = () => { const { t } = useTranslation(); const { colors } = useTheme(); @@ -386,7 +56,7 @@ const TabNavigator = () => { }}> ( @@ -394,19 +64,9 @@ const TabNavigator = () => { ), }} /> - ( - - ), - }} - /> ( @@ -416,7 +76,7 @@ const TabNavigator = () => { /> ( @@ -424,16 +84,6 @@ const TabNavigator = () => { ), }} /> - ( - 💰 - ), - }} - /> { const { isDark } = useTheme(); return ( - - - + + + + + ); }; + +export default AppNavigator; diff --git a/src/navigation/__tests__/AppNavigator.test.tsx b/src/navigation/__tests__/AppNavigator.test.tsx new file mode 100644 index 00000000..1fb45527 --- /dev/null +++ b/src/navigation/__tests__/AppNavigator.test.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import { AppNavigator } from '../AppNavigator'; + +jest.mock('../../theme', () => ({ + useTheme: () => ({ + isDark: false, + colors: { + navigation: { + tabBar: '#ffffff', + tabBarBorder: '#e0e0e0', + activeTab: '#007aff', + inactiveTab: '#8e8e93', + }, + }, + }), +})); + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe('AppNavigator Modular Architecture', () => { + it('renders without crashing with feature stacks', () => { + const { container } = render(); + expect(container).toBeDefined(); + }); +}); diff --git a/src/screens/CancellationFlowScreen.tsx b/src/screens/CancellationFlowScreen.tsx index 728845ce..b321e6d3 100644 --- a/src/screens/CancellationFlowScreen.tsx +++ b/src/screens/CancellationFlowScreen.tsx @@ -15,11 +15,6 @@ import { RootStackParamList } from '../navigation/types'; import { useCancellationStore } from '../store/cancellationStore'; import { useSubscriptionStore } from '../store'; -type Props = NativeStackScreenProps; - -const CancellationFlowScreen: React.FC = ({ route, navigation }) => { - const { currentStep, setReason, setStep, acceptOffer, reset } = useCancellationStore(); - const { deleteSubscription } = useSubscriptionStore(); import { useCancellationStore, CANCELLATION_REASONS } from '../store/cancellationStore'; import { RetentionOffer } from '../../backend/services/retentionService'; diff --git a/src/screens/SupportDashboardScreen.tsx b/src/screens/SupportDashboardScreen.tsx index 693d9251..c02412c2 100644 --- a/src/screens/SupportDashboardScreen.tsx +++ b/src/screens/SupportDashboardScreen.tsx @@ -126,6 +126,8 @@ const SupportDashboardScreen: React.FC = () => { {value} {label} {hint} + + ); const renderTicket = (ticket: SupportTicket) => ( {ticket.title} diff --git a/src/screens/__tests__/MerchantOnboardingScreen.test.tsx b/src/screens/__tests__/MerchantOnboardingScreen.test.tsx new file mode 100644 index 00000000..60cb7aa8 --- /dev/null +++ b/src/screens/__tests__/MerchantOnboardingScreen.test.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react-native'; +import MerchantOnboardingScreen from '../MerchantOnboardingScreen'; + +jest.mock('../../store/merchantStore', () => { + const mockState = { + onboarding: null, + isLoading: false, + error: null, + startOnboarding: jest.fn().mockResolvedValue(undefined), + nextStep: jest.fn().mockResolvedValue(undefined), + previousStep: jest.fn().mockResolvedValue(undefined), + requestVerification: jest.fn().mockResolvedValue(undefined), + uploadDocument: jest.fn().mockResolvedValue(undefined), + addNotification: jest.fn(), + getUnreadNotificationCount: jest.fn().mockReturnValue(0), + getOnboardingAnalytics: jest.fn().mockReturnValue({ + totalStarted: 0, + totalCompleted: 0, + totalRejected: 0, + completionRate: 0, + averageTimeToComplete: 0, + dropOffByStep: {}, + documentRejectionRate: 0, + averageVerificationTime: 0, + }), + }; + + return { + useMerchantStore: jest.fn(() => mockState), + }; +}); + +describe('MerchantOnboardingScreen', () => { + it('renders start card when onboarding has not started', () => { + const { getByText } = render(); + expect(getByText('Merchant Onboarding')).toBeTruthy(); + expect(getByText('Start Onboarding')).toBeTruthy(); + }); +}); diff --git a/src/store/subscriptionStore.ts b/src/store/subscriptionStore.ts index 263048ec..17b7425b 100644 --- a/src/store/subscriptionStore.ts +++ b/src/store/subscriptionStore.ts @@ -321,7 +321,7 @@ const debouncedAsyncStorage: StateStorage = { }, }; -interface SubscriptionState { +export interface SubscriptionState { subscriptions: Subscription[]; creditAccounts: Record; stats: SubscriptionStats; diff --git a/src/types/fraud.ts b/src/types/fraud.ts index c427e8c0..9a00f3b1 100644 --- a/src/types/fraud.ts +++ b/src/types/fraud.ts @@ -9,7 +9,6 @@ export type FraudSignalType = | 'geolocation-anomaly'; export type FraudReviewOutcome = 'true_positive' | 'false_positive' | 'needs_follow_up'; export type FraudEvidenceSource = 'payment' | 'device' | 'location' | 'support'; - | 'device-mismatch'; export interface FraudSignal { kind: FraudSignalType;