From b6ae19429a750196dac46e6288ffec101efd5202 Mon Sep 17 00:00:00 2001 From: manueldezman Date: Mon, 1 Jun 2026 14:28:16 +0100 Subject: [PATCH 1/3] feat(contracts): cap plans per merchant (#51) --- contracts/batch/src/lib.rs | 299 ++++++++- contracts/batch/tests/batch_tests.rs | 6 +- contracts/invoice/src/lib.rs | 9 +- contracts/proxy/src/lib.rs | 29 + ...contract_deploys_and_state_persists.1.json | 163 +++-- ..._contract_call_charges_subscription.1.json | 268 ++++++-- ...multiple_contract_interactions_work.1.json | 614 +++++++++++++----- ...s_actual_token_contract_for_charges.1.json | 268 ++++++-- ...serves_state_and_enforces_timelocks.1.json | 20 +- ...lure_does_not_change_implementation.1.json | 103 +-- contracts/proxy/tests/integration_soroban.rs | 59 ++ .../storage/src/transient_storage_tests.rs | 69 +- contracts/subscription/Cargo.toml | 5 +- contracts/subscription/src/lib.rs | 70 +- contracts/types/src/lib.rs | 18 +- 15 files changed, 1551 insertions(+), 449 deletions(-) diff --git a/contracts/batch/src/lib.rs b/contracts/batch/src/lib.rs index d06cce3c..2e34150e 100644 --- a/contracts/batch/src/lib.rs +++ b/contracts/batch/src/lib.rs @@ -1,38 +1,289 @@ #![no_std] +#![allow(clippy::too_many_arguments)] -use soroban_sdk::{contract, contractimpl, contracttype, Env, Vec, BytesN, Symbol, Address}; +use soroban_sdk::{ + contract, contracterror, contractimpl, contracttype, Address, Env, Vec, +}; + +const MAX_BATCH_ITEMS: u32 = 100; +const GAS_BASE: u64 = 50_000; +const GAS_PER_ITEM: u64 = 100_000; + +#[contracterror] +#[derive(Clone, Debug, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum BatchError { + InvalidBatch = 1, + AlreadyExecuted = 2, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum OperationType { + Create, + Charge, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct BatchOperation { + pub operation_type: OperationType, + pub subscription_ids: Vec, + pub params: Vec, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BatchState { + Pending, + Completed, + PartiallyCompleted, + Failed, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct BatchStatus { + pub state: BatchState, +} #[contracttype] -pub struct BatchItem { - pub account: Address, - pub amount: i128, - pub is_refund: bool, +#[derive(Clone, Debug, PartialEq)] +pub struct SubscriptionRecord { + pub id: u64, + pub charged: i128, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct BatchResult { + pub total_operations: u32, + pub successful_operations: u32, + pub failed_operations: u32, + pub gas_estimate: u64, + pub rolled_back: bool, +} + +#[contracttype] +#[derive(Clone)] +enum DataKey { + Admin, + BatchCount, + Batch(u64), + BatchOwner(u64), + BatchAtomic(u64), + BatchExecuted(u64), + BatchStatus(u64), + Subscription(u64), + History, +} + +pub fn validate_batch_operation(op: &BatchOperation) -> bool { + let n = op.subscription_ids.len(); + if n == 0 || n > MAX_BATCH_ITEMS { + return false; + } + match op.operation_type { + OperationType::Create => true, + OperationType::Charge => op.params.len() == n, + } +} + +pub fn estimate_batch_gas(op: &BatchOperation) -> u64 { + GAS_BASE + (op.subscription_ids.len() as u64 * GAS_PER_ITEM) } #[contract] -pub struct BatchTransactionContract; +pub struct SubTrackrBatch; #[contractimpl] -impl BatchTransactionContract { - /// Executes a batch of transactions with Merkle root verification. - pub fn execute_batch( +impl SubTrackrBatch { + pub fn initialize(env: Env, admin: Address) { + if env.storage().instance().has(&DataKey::Admin) { + return; + } + env.storage().instance().set(&DataKey::Admin, &admin); + env.storage().instance().set(&DataKey::BatchCount, &0u64); + env.storage() + .instance() + .set(&DataKey::History, &Vec::::new(&env)); + } + + pub fn seed_subscription(env: Env, subscription_id: u64) { + let sub = SubscriptionRecord { + id: subscription_id, + charged: 0, + }; + env.storage() + .persistent() + .set(&DataKey::Subscription(subscription_id), &sub); + } + + pub fn get_subscription(env: Env, subscription_id: u64) -> Option { + env.storage() + .persistent() + .get(&DataKey::Subscription(subscription_id)) + } + + pub fn create_batch_operation( env: Env, - items: Vec, - merkle_root: BytesN<32>, - ) -> bool { - // Basic batch processing logic handling both charges and refunds - // Also supports partial batch failure isolation (mock implementation) - for item in items.iter() { - if item.is_refund { - // Execute refund logic - env.events().publish((Symbol::new(&env, "refund_executed"),), item.amount); - } else { - // Execute charge logic - env.events().publish((Symbol::new(&env, "charge_executed"),), item.amount); + owner: Address, + operation: BatchOperation, + atomic: bool, + ) -> Result { + owner.require_auth(); + if !validate_batch_operation(&operation) { + return Err(BatchError::InvalidBatch); + } + + let mut count: u64 = env.storage().instance().get(&DataKey::BatchCount).unwrap_or(0); + count += 1; + env.storage().instance().set(&DataKey::BatchCount, &count); + + env.storage() + .persistent() + .set(&DataKey::Batch(count), &operation); + env.storage() + .persistent() + .set(&DataKey::BatchOwner(count), &owner); + env.storage() + .persistent() + .set(&DataKey::BatchAtomic(count), &atomic); + env.storage() + .persistent() + .set(&DataKey::BatchExecuted(count), &false); + env.storage() + .persistent() + .set(&DataKey::BatchStatus(count), &BatchStatus { state: BatchState::Pending }); + + let mut history: Vec = env.storage().instance().get(&DataKey::History).unwrap(); + history.push_back(count); + env.storage().instance().set(&DataKey::History, &history); + + Ok(count) + } + + pub fn get_batch_history(env: Env) -> Vec { + env.storage() + .instance() + .get(&DataKey::History) + .unwrap_or(Vec::new(&env)) + } + + pub fn get_batch_status(env: Env, batch_id: u64) -> BatchStatus { + env.storage() + .persistent() + .get(&DataKey::BatchStatus(batch_id)) + .unwrap_or(BatchStatus { + state: BatchState::Pending, + }) + } + + pub fn execute_batch(env: Env, batch_id: u64) -> Result { + let executed: bool = env + .storage() + .persistent() + .get(&DataKey::BatchExecuted(batch_id)) + .unwrap_or(false); + if executed { + return Err(BatchError::AlreadyExecuted); + } + + let op: BatchOperation = env + .storage() + .persistent() + .get(&DataKey::Batch(batch_id)) + .ok_or(BatchError::InvalidBatch)?; + let atomic: bool = env + .storage() + .persistent() + .get(&DataKey::BatchAtomic(batch_id)) + .unwrap_or(false); + + let total = op.subscription_ids.len() as u32; + let gas_estimate = estimate_batch_gas(&op); + + let mut successful: u32 = 0; + let mut failed: u32 = 0; + + // Minimal rollback model used by tests: if atomic and any failure occurs, + // we do not persist any successful effects. + let mut staged: Vec = Vec::new(&env); + + for (i, sub_id) in op.subscription_ids.iter().enumerate() { + let idx: u32 = i as u32; + match op.operation_type { + OperationType::Create => { + let sub = SubscriptionRecord { id: sub_id, charged: 0 }; + if atomic { + staged.push_back(sub); + } else { + env.storage() + .persistent() + .set(&DataKey::Subscription(sub_id), &sub); + } + successful += 1; + } + OperationType::Charge => { + let existing: Option = env + .storage() + .persistent() + .get(&DataKey::Subscription(sub_id)); + if existing.is_none() { + failed += 1; + if atomic { + // Any failure aborts for atomic batches. + break; + } + continue; + } + let mut sub = existing.unwrap(); + let amount = op.params.get(idx).unwrap_or(0); + sub.charged += amount; + if atomic { + staged.push_back(sub); + } else { + env.storage() + .persistent() + .set(&DataKey::Subscription(sub_id), &sub); + } + successful += 1; + } + } + } + + let rolled_back = atomic && failed > 0; + if rolled_back { + successful = 0; + } else if atomic { + for sub in staged.iter() { + env.storage() + .persistent() + .set(&DataKey::Subscription(sub.id), &sub); } } - - // Return true if the batch processed successfully - true + + let state = if rolled_back { + BatchState::Failed + } else if failed == 0 { + BatchState::Completed + } else { + BatchState::PartiallyCompleted + }; + + env.storage() + .persistent() + .set(&DataKey::BatchExecuted(batch_id), &true); + env.storage() + .persistent() + .set(&DataKey::BatchStatus(batch_id), &BatchStatus { state }); + + Ok(BatchResult { + total_operations: total, + successful_operations: successful, + failed_operations: failed, + gas_estimate, + rolled_back, + }) } } diff --git a/contracts/batch/tests/batch_tests.rs b/contracts/batch/tests/batch_tests.rs index 0114d12e..117cb410 100644 --- a/contracts/batch/tests/batch_tests.rs +++ b/contracts/batch/tests/batch_tests.rs @@ -153,8 +153,10 @@ fn rejects_invalid_batch_creation() { fn records_audit_history() { let (env, client, _admin) = setup(); let owner = Address::generate(&env); - let a = client.create_batch_operation(&owner, &op(&env, OperationType::Create, &[1], &[]), &false); - let b = client.create_batch_operation(&owner, &op(&env, OperationType::Create, &[2], &[]), &false); + let a = + client.create_batch_operation(&owner, &op(&env, OperationType::Create, &[1], &[]), &false); + let b = + client.create_batch_operation(&owner, &op(&env, OperationType::Create, &[2], &[]), &false); let history = client.get_batch_history(); assert_eq!(history, vec![&env, a, b]); } diff --git a/contracts/invoice/src/lib.rs b/contracts/invoice/src/lib.rs index 6f675a61..b949f381 100644 --- a/contracts/invoice/src/lib.rs +++ b/contracts/invoice/src/lib.rs @@ -10,8 +10,9 @@ use alloc::vec; use soroban_sdk::{Address, Bytes, Env, IntoVal, String, TryFromVal, Val, Vec}; use subtrackr_types::{ CustomerTaxStatus, DigitalGoodsClass, Invoice, InvoiceConfig, InvoiceLineItem, InvoiceStatus, - Plan, StorageKey, Subscription, TaxRateChangeEvent, TaxRateEntry, TaxRemittanceLineItem, - TaxRemittanceReport, TaxType, TaxJurisdiction, TimeRange, TaxReportLineItem, RemittanceStatus, + MaybeDigitalGoodsClass, Plan, StorageKey, Subscription, TaxRateChangeEvent, TaxRateEntry, + TaxRemittanceLineItem, TaxRemittanceReport, TaxType, TaxJurisdiction, TimeRange, + TaxReportLineItem, RemittanceStatus, }; const DEFAULT_RATE_SCALE: i128 = 1_000_000; @@ -179,7 +180,7 @@ fn get_customer_tax_status(env: &Env, subscriber: &Address) -> CustomerTaxStatus certificate_expiry: 0, issuing_authority: String::from_str(env, ""), exempt_jurisdictions: Vec::new(env), - digital_goods_override: None, + digital_goods_override: MaybeDigitalGoodsClass::None, }) } @@ -611,7 +612,7 @@ impl SubTrackrInvoice { certificate_expiry: u64, issuing_authority: String, exempt_jurisdictions: Vec, - digital_goods_override: Option, + digital_goods_override: MaybeDigitalGoodsClass, ) { let stored_admin = get_admin(&env); assert!(admin == stored_admin, "Admin mismatch"); diff --git a/contracts/proxy/src/lib.rs b/contracts/proxy/src/lib.rs index 61a0dcfe..08da23be 100644 --- a/contracts/proxy/src/lib.rs +++ b/contracts/proxy/src/lib.rs @@ -377,6 +377,35 @@ impl UpgradeableProxy { ); } + pub fn set_max_plans_per_merchant(env: Env, new_limit: u32) { + let proxy_addr = current_proxy_address(&env); + let storage_addr = proxy_storage::storage_address(&env); + invoke_impl::<()>( + &env, + "set_max_plans_per_merchant", + soroban_sdk::vec![ + &env, + proxy_addr.into_val(&env), + storage_addr.into_val(&env), + new_limit.into_val(&env) + ], + ); + } + + pub fn get_max_plans_per_merchant(env: Env) -> u32 { + let proxy_addr = current_proxy_address(&env); + let storage_addr = proxy_storage::storage_address(&env); + invoke_impl( + &env, + "get_max_plans_per_merchant", + soroban_sdk::vec![ + &env, + proxy_addr.into_val(&env), + storage_addr.into_val(&env) + ], + ) + } + pub fn create_plan( env: Env, merchant: Address, diff --git a/contracts/proxy/test_snapshots/integration_contract_deploys_and_state_persists.1.json b/contracts/proxy/test_snapshots/integration_contract_deploys_and_state_persists.1.json index 4eaf12ba..28ea604b 100644 --- a/contracts/proxy/test_snapshots/integration_contract_deploys_and_state_persists.1.json +++ b/contracts/proxy/test_snapshots/integration_contract_deploys_and_state_persists.1.json @@ -964,7 +964,7 @@ "key": { "vec": [ { - "symbol": "ProxyPreviousImplementationCount" + "symbol": "ProxyPrevImplCount" } ] }, @@ -1844,6 +1844,57 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "instance_get" + } + ], + "data": { + "vec": [ + { + "symbol": "MaxPlansPerMerchant" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "instance_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -1897,6 +1948,60 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "persistent_get" + } + ], + "data": { + "vec": [ + { + "symbol": "MerchantPlans" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "persistent_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -2096,60 +2201,6 @@ }, "failed_call": false }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000005" - }, - { - "symbol": "persistent_get" - } - ], - "data": { - "vec": [ - { - "symbol": "MerchantPlans" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "persistent_get" - } - ], - "data": "void" - } - } - }, - "failed_call": false - }, { "event": { "ext": "v0", @@ -4692,4 +4743,4 @@ "failed_call": false } ] -} +} \ No newline at end of file diff --git a/contracts/proxy/test_snapshots/integration_cross_contract_call_charges_subscription.1.json b/contracts/proxy/test_snapshots/integration_cross_contract_call_charges_subscription.1.json index e5de837b..1fc7af90 100644 --- a/contracts/proxy/test_snapshots/integration_cross_contract_call_charges_subscription.1.json +++ b/contracts/proxy/test_snapshots/integration_cross_contract_call_charges_subscription.1.json @@ -1300,7 +1300,7 @@ "key": { "vec": [ { - "symbol": "ProxyPreviousImplementationCount" + "symbol": "ProxyPrevImplCount" } ] }, @@ -2285,6 +2285,57 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "instance_get" + } + ], + "data": { + "vec": [ + { + "symbol": "MaxPlansPerMerchant" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "instance_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -2338,6 +2389,60 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "persistent_get" + } + ], + "data": { + "vec": [ + { + "symbol": "MerchantPlans" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "persistent_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -2537,60 +2642,6 @@ }, "failed_call": false }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000005" - }, - { - "symbol": "persistent_get" - } - ], - "data": { - "vec": [ - { - "symbol": "MerchantPlans" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "persistent_get" - } - ], - "data": "void" - } - } - }, - "failed_call": false - }, { "event": { "ext": "v0", @@ -4240,6 +4291,111 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "instance_get" + } + ], + "data": { + "vec": [ + { + "symbol": "OracleContract" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "instance_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "persistent_get" + } + ], + "data": { + "vec": [ + { + "symbol": "PriceBounds" + }, + { + "u64": 1 + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "persistent_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -5709,4 +5865,4 @@ "failed_call": false } ] -} +} \ No newline at end of file diff --git a/contracts/proxy/test_snapshots/integration_multiple_contract_interactions_work.1.json b/contracts/proxy/test_snapshots/integration_multiple_contract_interactions_work.1.json index 58b112a5..5a469a0c 100644 --- a/contracts/proxy/test_snapshots/integration_multiple_contract_interactions_work.1.json +++ b/contracts/proxy/test_snapshots/integration_multiple_contract_interactions_work.1.json @@ -2241,7 +2241,7 @@ "key": { "vec": [ { - "symbol": "ProxyPreviousImplementationCount" + "symbol": "ProxyPrevImplCount" } ] }, @@ -3518,6 +3518,57 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "instance_get" + } + ], + "data": { + "vec": [ + { + "symbol": "MaxPlansPerMerchant" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "instance_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -3571,6 +3622,60 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "persistent_get" + } + ], + "data": { + "vec": [ + { + "symbol": "MerchantPlans" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "persistent_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -3770,60 +3875,6 @@ }, "failed_call": false }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000005" - }, - { - "symbol": "persistent_get" - } - ], - "data": { - "vec": [ - { - "symbol": "MerchantPlans" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "persistent_get" - } - ], - "data": "void" - } - } - }, - "failed_call": false - }, { "event": { "ext": "v0", @@ -5384,6 +5435,57 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "instance_get" + } + ], + "data": { + "vec": [ + { + "symbol": "MaxPlansPerMerchant" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "instance_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -5437,6 +5539,60 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "persistent_get" + } + ], + "data": { + "vec": [ + { + "symbol": "MerchantPlans" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "persistent_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -5514,99 +5670,41 @@ } }, { - "key": { - "symbol": "name" - }, - "val": { - "string": "Premium Plan" - } - }, - { - "key": { - "symbol": "price" - }, - "val": { - "i128": { - "hi": 0, - "lo": 900 - } - } - }, - { - "key": { - "symbol": "subscriber_count" - }, - "val": { - "u32": 0 - } - }, - { - "key": { - "symbol": "token" - }, - "val": { - "address": "CCVQTUQIJR624NNEI5TORM2BHEXTSDMY5ZB3CYJKAATGJQCY7LU2MD45" - } - } - ] - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "persistent_set" - } - ], - "data": "void" - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000005" - }, - { - "symbol": "instance_set" - } - ], - "data": { - "vec": [ - { - "vec": [ + "key": { + "symbol": "name" + }, + "val": { + "string": "Premium Plan" + } + }, { - "symbol": "PlanCount" + "key": { + "symbol": "price" + }, + "val": { + "i128": { + "hi": 0, + "lo": 900 + } + } + }, + { + "key": { + "symbol": "subscriber_count" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "token" + }, + "val": { + "address": "CCVQTUQIJR624NNEI5TORM2BHEXTSDMY5ZB3CYJKAATGJQCY7LU2MD45" + } } ] - }, - { - "u64": 2 } ] } @@ -5627,7 +5725,7 @@ "symbol": "fn_return" }, { - "symbol": "instance_set" + "symbol": "persistent_set" } ], "data": "void" @@ -5651,16 +5749,20 @@ "bytes": "0000000000000000000000000000000000000000000000000000000000000005" }, { - "symbol": "persistent_get" + "symbol": "instance_set" } ], "data": { "vec": [ { - "symbol": "MerchantPlans" + "vec": [ + { + "symbol": "PlanCount" + } + ] }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "u64": 2 } ] } @@ -5681,7 +5783,7 @@ "symbol": "fn_return" }, { - "symbol": "persistent_get" + "symbol": "instance_set" } ], "data": "void" @@ -7315,6 +7417,111 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "instance_get" + } + ], + "data": { + "vec": [ + { + "symbol": "OracleContract" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "instance_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "persistent_get" + } + ], + "data": { + "vec": [ + { + "symbol": "PriceBounds" + }, + { + "u64": 1 + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "persistent_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -8729,6 +8936,111 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "instance_get" + } + ], + "data": { + "vec": [ + { + "symbol": "OracleContract" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "instance_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "persistent_get" + } + ], + "data": { + "vec": [ + { + "symbol": "PriceBounds" + }, + { + "u64": 2 + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "persistent_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -10790,4 +11102,4 @@ "failed_call": false } ] -} +} \ No newline at end of file diff --git a/contracts/proxy/test_snapshots/integration_uses_actual_token_contract_for_charges.1.json b/contracts/proxy/test_snapshots/integration_uses_actual_token_contract_for_charges.1.json index 0e04372a..fff025c5 100644 --- a/contracts/proxy/test_snapshots/integration_uses_actual_token_contract_for_charges.1.json +++ b/contracts/proxy/test_snapshots/integration_uses_actual_token_contract_for_charges.1.json @@ -1304,7 +1304,7 @@ "key": { "vec": [ { - "symbol": "ProxyPreviousImplementationCount" + "symbol": "ProxyPrevImplCount" } ] }, @@ -2257,6 +2257,57 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "instance_get" + } + ], + "data": { + "vec": [ + { + "symbol": "MaxPlansPerMerchant" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "instance_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -2310,6 +2361,60 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "persistent_get" + } + ], + "data": { + "vec": [ + { + "symbol": "MerchantPlans" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "persistent_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -2509,60 +2614,6 @@ }, "failed_call": false }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_call" - }, - { - "bytes": "0000000000000000000000000000000000000000000000000000000000000005" - }, - { - "symbol": "persistent_get" - } - ], - "data": { - "vec": [ - { - "symbol": "MerchantPlans" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - ] - } - } - } - }, - "failed_call": false - }, - { - "event": { - "ext": "v0", - "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", - "type_": "diagnostic", - "body": { - "v0": { - "topics": [ - { - "symbol": "fn_return" - }, - { - "symbol": "persistent_get" - } - ], - "data": "void" - } - } - }, - "failed_call": false - }, { "event": { "ext": "v0", @@ -4283,6 +4334,111 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "instance_get" + } + ], + "data": { + "vec": [ + { + "symbol": "OracleContract" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "instance_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000006", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "symbol": "persistent_get" + } + ], + "data": { + "vec": [ + { + "symbol": "PriceBounds" + }, + { + "u64": 1 + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000005", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "persistent_get" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -5835,4 +5991,4 @@ "failed_call": false } ] -} +} \ No newline at end of file diff --git a/contracts/proxy/test_snapshots/upgrade_flow_preserves_state_and_enforces_timelocks.1.json b/contracts/proxy/test_snapshots/upgrade_flow_preserves_state_and_enforces_timelocks.1.json index 9e5dfc05..05cee692 100644 --- a/contracts/proxy/test_snapshots/upgrade_flow_preserves_state_and_enforces_timelocks.1.json +++ b/contracts/proxy/test_snapshots/upgrade_flow_preserves_state_and_enforces_timelocks.1.json @@ -1626,27 +1626,27 @@ "key": { "vec": [ { - "symbol": "ProxyPreviousImplementation" - }, - { - "u32": 0 + "symbol": "ProxyPrevImplCount" } ] }, "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + "u32": 1 } }, { "key": { "vec": [ { - "symbol": "ProxyPreviousImplementationCount" + "symbol": "ProxyPreviousImplementation" + }, + { + "u32": 0 } ] }, "val": { - "u32": 1 + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" } }, { @@ -3968,7 +3968,7 @@ "data": { "vec": [ { - "string": "caught panic 'Upgrade timelock not expired' from contract function 'Symbol(obj#1021)'" + "string": "caught panic 'Upgrade timelock not expired' from contract function 'Symbol(obj#989)'" }, { "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" @@ -6023,7 +6023,7 @@ "data": { "vec": [ { - "string": "caught panic 'Upgrade timelock not expired' from contract function 'Symbol(obj#1953)'" + "string": "caught panic 'Upgrade timelock not expired' from contract function 'Symbol(obj#1917)'" }, { "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" @@ -6414,4 +6414,4 @@ "failed_call": false } ] -} +} \ No newline at end of file diff --git a/contracts/proxy/test_snapshots/upgrade_validation_failure_does_not_change_implementation.1.json b/contracts/proxy/test_snapshots/upgrade_validation_failure_does_not_change_implementation.1.json index b9965b47..3730b4c3 100644 --- a/contracts/proxy/test_snapshots/upgrade_validation_failure_does_not_change_implementation.1.json +++ b/contracts/proxy/test_snapshots/upgrade_validation_failure_does_not_change_implementation.1.json @@ -481,6 +481,62 @@ 4095 ] ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "vec": [ + { + "symbol": "ProxyScheduledUpgrade" + } + ] + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "vec": [ + { + "symbol": "ProxyScheduledUpgrade" + } + ] + }, + "durability": "temporary", + "val": { + "map": [ + { + "key": { + "symbol": "execute_after" + }, + "val": { + "u64": 1700000201 + } + }, + { + "key": { + "symbol": "implementation" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + } + ] + } + } + }, + "ext": "v0" + }, + 120960 + ] + ], [ { "contract_data": { @@ -838,27 +894,27 @@ "key": { "vec": [ { - "symbol": "ProxyPreviousImplementation" - }, - { - "u32": 0 + "symbol": "ProxyPrevImplCount" } ] }, "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + "u32": 1 } }, { "key": { "vec": [ { - "symbol": "ProxyPreviousImplementationCount" + "symbol": "ProxyPreviousImplementation" + }, + { + "u32": 0 } ] }, "val": { - "u32": 1 + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" } }, { @@ -873,35 +929,6 @@ "u64": 50 } }, - { - "key": { - "vec": [ - { - "symbol": "ProxyScheduledUpgrade" - } - ] - }, - "val": { - "map": [ - { - "key": { - "symbol": "execute_after" - }, - "val": { - "u64": 1700000201 - } - }, - { - "key": { - "symbol": "implementation" - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" - } - } - ] - } - }, { "key": { "vec": [ @@ -2489,7 +2516,7 @@ "data": { "vec": [ { - "string": "caught panic 'incompatible state' from contract function 'Symbol(obj#1043)'" + "string": "caught panic 'incompatible state' from contract function 'Symbol(obj#965)'" }, { "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" @@ -2803,4 +2830,4 @@ "failed_call": false } ] -} +} \ No newline at end of file diff --git a/contracts/proxy/tests/integration_soroban.rs b/contracts/proxy/tests/integration_soroban.rs index 53fec2a9..78f7a3e8 100644 --- a/contracts/proxy/tests/integration_soroban.rs +++ b/contracts/proxy/tests/integration_soroban.rs @@ -80,6 +80,24 @@ fn setup_integration() -> IntegrationSetup { } } +fn setup_proxy_only() -> (Env, Address, Address, Address) { + let env = Env::default(); + env.mock_all_auths_allowing_non_root_auth(); + env.ledger().set_timestamp(1_700_000_000); + + let admin = Address::generate(&env); + let merchant = Address::generate(&env); + + let storage_id = env.register_contract(None, SubTrackrStorage); + let implementation_id = env.register_contract(None, SubTrackrSubscription); + + let proxy_id = env.register_contract(None, UpgradeableProxy); + let proxy = UpgradeableProxyClient::new(&env, &proxy_id); + proxy.initialize(&admin, &storage_id, &implementation_id, &0u64, &0u64); + + (env, proxy_id, admin, merchant) +} + #[test] fn integration_contract_deploys_and_state_persists() { let setup = setup_integration(); @@ -196,3 +214,44 @@ fn integration_multiple_contract_interactions_work() { assert_eq!(token.balance(&setup.merchant), 500); assert_eq!(second_token.balance(&second_merchant), 900); } + +#[test] +fn integration_plan_limit_blocks_third_plan() { + let (env, proxy_id, _admin, merchant) = setup_proxy_only(); + let proxy = UpgradeableProxyClient::new(&env, &proxy_id); + + proxy.set_max_plans_per_merchant(&2u32); + + let token_admin = Address::generate(&env); + let token_id = env.register_stellar_asset_contract_v2(token_admin); + + let name = String::from_str(&env, "Limited Plan"); + proxy.create_plan(&merchant, &name, &500, &token_id.address(), &Interval::Monthly); + proxy.create_plan(&merchant, &name, &600, &token_id.address(), &Interval::Monthly); + + let res = proxy.try_create_plan(&merchant, &name, &700, &token_id.address(), &Interval::Monthly); + assert!(res.is_err()); +} + +#[test] +fn integration_lowering_plan_limit_does_not_affect_existing_plans() { + let (env, proxy_id, _admin, merchant) = setup_proxy_only(); + let proxy = UpgradeableProxyClient::new(&env, &proxy_id); + + let token_admin = Address::generate(&env); + let token_id = env.register_stellar_asset_contract_v2(token_admin); + let name = String::from_str(&env, "Plan"); + + let p1 = proxy.create_plan(&merchant, &name, &100, &token_id.address(), &Interval::Monthly); + let p2 = proxy.create_plan(&merchant, &name, &200, &token_id.address(), &Interval::Monthly); + let p3 = proxy.create_plan(&merchant, &name, &300, &token_id.address(), &Interval::Monthly); + + proxy.set_max_plans_per_merchant(&2u32); + + assert!(proxy.get_plan(&p1).active); + assert!(proxy.get_plan(&p2).active); + assert!(proxy.get_plan(&p3).active); + + let res = proxy.try_create_plan(&merchant, &name, &400, &token_id.address(), &Interval::Monthly); + assert!(res.is_err()); +} diff --git a/contracts/storage/src/transient_storage_tests.rs b/contracts/storage/src/transient_storage_tests.rs index af362065..ddf18d65 100644 --- a/contracts/storage/src/transient_storage_tests.rs +++ b/contracts/storage/src/transient_storage_tests.rs @@ -11,10 +11,10 @@ #[cfg(test)] mod transient_storage_tests { - use crate::SubTrackrStorage; + use crate::{SubTrackrStorage, SubTrackrStorageClient}; use soroban_sdk::{ testutils::{Address as _, Ledger, LedgerInfo}, - Address, Env, String as SorobanString, + Address, Env, IntoVal, String as SorobanString, TryFromVal, }; use subtrackr_types::StorageKey; @@ -23,12 +23,17 @@ mod transient_storage_tests { fn setup() -> (Env, Address, Address) { let env = Env::default(); env.mock_all_auths(); + // Configure a small minimum TTL for temporary entries so expiry is testable. + env.ledger().set(LedgerInfo { + min_temp_entry_ttl: 1, + ..env.ledger().get() + }); let admin = Address::generate(&env); let implementation = Address::generate(&env); let contract_id = env.register_contract(None, SubTrackrStorage); - let client = soroban_sdk::contract_client!(env, SubTrackrStorage, &contract_id); + let client = SubTrackrStorageClient::new(&env, &contract_id); client.initialize(&admin, &implementation); (env, contract_id, implementation) @@ -39,7 +44,7 @@ mod transient_storage_tests { #[test] fn test_temporary_set_and_get_roundtrip() { let (env, contract_id, _impl) = setup(); - let client = soroban_sdk::contract_client!(env, SubTrackrStorage, &contract_id); + let client = SubTrackrStorageClient::new(&env, &contract_id); let caller = Address::generate(&env); let fname = SorobanString::from_str(&env, "subscribe"); @@ -47,18 +52,18 @@ mod transient_storage_tests { let timestamp: u64 = 1_000_000; // Write with a 12-ledger TTL (≈ 60 s) - client.temporary_set(&key, ×tamp.into(), &12u32); + client.temporary_set(&key, ×tamp.into_val(&env), &12u32); let result: Option = client .temporary_get(&key) - .map(|v| soroban_sdk::TryFromVal::try_from_val(&env, &v).unwrap()); + .map(|v| TryFromVal::try_from_val(&env, &v).unwrap()); assert_eq!(result, Some(timestamp)); } #[test] fn test_temporary_get_returns_none_for_missing_key() { let (env, contract_id, _impl) = setup(); - let client = soroban_sdk::contract_client!(env, SubTrackrStorage, &contract_id); + let client = SubTrackrStorageClient::new(&env, &contract_id); let caller = Address::generate(&env); let fname = SorobanString::from_str(&env, "nonexistent"); @@ -71,14 +76,14 @@ mod transient_storage_tests { #[test] fn test_temporary_remove_clears_entry() { let (env, contract_id, _impl) = setup(); - let client = soroban_sdk::contract_client!(env, SubTrackrStorage, &contract_id); + let client = SubTrackrStorageClient::new(&env, &contract_id); let caller = Address::generate(&env); let fname = SorobanString::from_str(&env, "cancel_subscription"); let key = StorageKey::TmpLastCall(caller, fname); let ts: u64 = 999; - client.temporary_set(&key, &ts.into(), &10u32); + client.temporary_set(&key, &ts.into_val(&env), &10u32); assert!(client.temporary_get(&key).is_some()); client.temporary_remove(&key); @@ -90,21 +95,21 @@ mod transient_storage_tests { #[test] fn test_temporary_entry_expires_after_ttl() { let (env, contract_id, _impl) = setup(); - let client = soroban_sdk::contract_client!(env, SubTrackrStorage, &contract_id); + let client = SubTrackrStorageClient::new(&env, &contract_id); let caller = Address::generate(&env); let fname = SorobanString::from_str(&env, "create_plan"); let key = StorageKey::TmpLastCall(caller, fname); let ts: u64 = 500; - // Write with TTL = 5 ledgers - client.temporary_set(&key, &ts.into(), &5u32); + // Write with TTL = 5 ledgers. + client.temporary_set(&key, &ts.into_val(&env), &5u32); assert!( client.temporary_get(&key).is_some(), "entry should exist before expiry" ); - // Advance ledger sequence past the TTL + // Advance ledger sequence past the TTL. env.ledger().set(LedgerInfo { sequence_number: env.ledger().sequence() + 6, ..env.ledger().get() @@ -122,7 +127,7 @@ mod transient_storage_tests { #[test] fn test_tmp_last_call_keys_are_isolated_per_caller() { let (env, contract_id, _impl) = setup(); - let client = soroban_sdk::contract_client!(env, SubTrackrStorage, &contract_id); + let client = SubTrackrStorageClient::new(&env, &contract_id); let caller_a = Address::generate(&env); let caller_b = Address::generate(&env); @@ -131,8 +136,8 @@ mod transient_storage_tests { let key_a = StorageKey::TmpLastCall(caller_a.clone(), fname.clone()); let key_b = StorageKey::TmpLastCall(caller_b.clone(), fname.clone()); - client.temporary_set(&key_a, &(100u64).into(), &20u32); - client.temporary_set(&key_b, &(200u64).into(), &20u32); + client.temporary_set(&key_a, &100u64.into_val(&env), &20u32); + client.temporary_set(&key_b, &200u64.into_val(&env), &20u32); let val_a: u64 = soroban_sdk::TryFromVal::try_from_val(&env, &client.temporary_get(&key_a).unwrap()) @@ -148,7 +153,7 @@ mod transient_storage_tests { #[test] fn test_tmp_last_call_keys_are_isolated_per_function() { let (env, contract_id, _impl) = setup(); - let client = soroban_sdk::contract_client!(env, SubTrackrStorage, &contract_id); + let client = SubTrackrStorageClient::new(&env, &contract_id); let caller = Address::generate(&env); let fname_a = SorobanString::from_str(&env, "subscribe"); @@ -157,8 +162,8 @@ mod transient_storage_tests { let key_a = StorageKey::TmpLastCall(caller.clone(), fname_a); let key_b = StorageKey::TmpLastCall(caller.clone(), fname_b); - client.temporary_set(&key_a, &(111u64).into(), &20u32); - client.temporary_set(&key_b, &(222u64).into(), &20u32); + client.temporary_set(&key_a, &111u64.into_val(&env), &20u32); + client.temporary_set(&key_b, &222u64.into_val(&env), &20u32); let val_a: u64 = soroban_sdk::TryFromVal::try_from_val(&env, &client.temporary_get(&key_a).unwrap()) @@ -176,7 +181,7 @@ mod transient_storage_tests { #[test] fn test_proxy_scheduled_upgrade_stored_in_temporary() { let (env, contract_id, _impl) = setup(); - let client = soroban_sdk::contract_client!(env, SubTrackrStorage, &contract_id); + let client = SubTrackrStorageClient::new(&env, &contract_id); let new_impl = Address::generate(&env); let execute_after: u64 = env.ledger().timestamp() + 86_400; // +1 day @@ -188,11 +193,11 @@ mod transient_storage_tests { let key = StorageKey::ProxyScheduledUpgrade; // TTL = 120 960 ledgers (≈ 7 days) - client.temporary_set(&key, &upgrade.into(), &120_960u32); + client.temporary_set(&key, &upgrade.into_val(&env), &120_960u32); let stored: Option = client .temporary_get(&key) - .map(|v| soroban_sdk::TryFromVal::try_from_val(&env, &v).unwrap()); + .map(|v| TryFromVal::try_from_val(&env, &v).unwrap()); assert!(stored.is_some()); let stored = stored.unwrap(); @@ -203,7 +208,7 @@ mod transient_storage_tests { #[test] fn test_proxy_scheduled_upgrade_cleared_after_execution() { let (env, contract_id, _impl) = setup(); - let client = soroban_sdk::contract_client!(env, SubTrackrStorage, &contract_id); + let client = SubTrackrStorageClient::new(&env, &contract_id); let new_impl = Address::generate(&env); let upgrade = subtrackr_types::ScheduledUpgrade { @@ -212,7 +217,7 @@ mod transient_storage_tests { }; let key = StorageKey::ProxyScheduledUpgrade; - client.temporary_set(&key, &upgrade.into(), &120_960u32); + client.temporary_set(&key, &upgrade.into_val(&env), &120_960u32); assert!(client.temporary_get(&key).is_some()); // Simulate upgrade execution: clear the entry @@ -225,26 +230,26 @@ mod transient_storage_tests { #[test] fn test_persistent_storage_unaffected_by_transient_changes() { let (env, contract_id, _impl) = setup(); - let client = soroban_sdk::contract_client!(env, SubTrackrStorage, &contract_id); + let client = SubTrackrStorageClient::new(&env, &contract_id); // Write a persistent value let plan_id: u64 = 1; let key = StorageKey::Plan(plan_id); // We can't easily write a full Plan here without the subscription crate, // so we write a simple u64 to verify the persistent bridge is unaffected. - client.persistent_set(&key, &(42u64).into()); + client.persistent_set(&key, &42u64.into_val(&env)); // Write and remove a temporary value with the same numeric id let caller = Address::generate(&env); let fname = SorobanString::from_str(&env, "test"); let tmp_key = StorageKey::TmpLastCall(caller, fname); - client.temporary_set(&tmp_key, &(99u64).into(), &5u32); + client.temporary_set(&tmp_key, &99u64.into_val(&env), &5u32); client.temporary_remove(&tmp_key); // Persistent value must still be intact let persisted: Option = client .persistent_get(&key) - .map(|v| soroban_sdk::TryFromVal::try_from_val(&env, &v).unwrap()); + .map(|v| TryFromVal::try_from_val(&env, &v).unwrap()); assert_eq!(persisted, Some(42u64)); } @@ -253,7 +258,7 @@ mod transient_storage_tests { #[test] fn test_instance_storage_unaffected_by_transient_changes() { let (env, contract_id, _impl) = setup(); - let client = soroban_sdk::contract_client!(env, SubTrackrStorage, &contract_id); + let client = SubTrackrStorageClient::new(&env, &contract_id); // Admin is set during initialize() in instance storage let admin_from_instance = client.get_admin(); @@ -262,7 +267,7 @@ mod transient_storage_tests { let caller = Address::generate(&env); let fname = SorobanString::from_str(&env, "test_fn"); let tmp_key = StorageKey::TmpLastCall(caller, fname); - client.temporary_set(&tmp_key, &(1u64).into(), &1u32); + client.temporary_set(&tmp_key, &1u64.into_val(&env), &1u32); // Advance past TTL env.ledger().set(LedgerInfo { @@ -279,14 +284,14 @@ mod transient_storage_tests { #[test] fn test_minimum_ttl_is_one_ledger() { let (env, contract_id, _impl) = setup(); - let client = soroban_sdk::contract_client!(env, SubTrackrStorage, &contract_id); + let client = SubTrackrStorageClient::new(&env, &contract_id); let caller = Address::generate(&env); let fname = SorobanString::from_str(&env, "fn"); let key = StorageKey::TmpLastCall(caller, fname); // TTL = 0 should be treated as 1 ledger (minimum) - client.temporary_set(&key, &(1u64).into(), &0u32); + client.temporary_set(&key, &1u64.into_val(&env), &0u32); assert!(client.temporary_get(&key).is_some()); } } diff --git a/contracts/subscription/Cargo.toml b/contracts/subscription/Cargo.toml index 53584cf3..08acd6ef 100644 --- a/contracts/subscription/Cargo.toml +++ b/contracts/subscription/Cargo.toml @@ -10,12 +10,13 @@ name = "subtrackr_subscription" path = "src/lib.rs" crate-type = ["cdylib", "rlib"] -serde = "1.0" - [dependencies] soroban-sdk = "21.0.0" subtrackr-types = { path = "../types" } subtrackr-oracle = { path = "../oracle" } +[features] +extended = [] + [dev-dependencies] soroban-sdk = { version = "21.0.0", features = ["testutils"] } diff --git a/contracts/subscription/src/lib.rs b/contracts/subscription/src/lib.rs index bf120bcf..4a2ce79d 100644 --- a/contracts/subscription/src/lib.rs +++ b/contracts/subscription/src/lib.rs @@ -2,6 +2,7 @@ mod gas_optimization; mod gas_profiler; mod gas_storage; +mod revenue; use soroban_sdk::{token, Address, Env, IntoVal, String, Symbol, TryFromVal, Val, Vec}; use subtrackr_oracle::{OracleError, SubTrackrOracleClient}; use subtrackr_types::{ @@ -12,6 +13,10 @@ use subtrackr_types::{ /// Billing interval in seconds. const MAX_PAUSE_DURATION: u64 = 2_592_000; // 30 days +/// Default maximum number of plans a merchant can create. +/// This can be overridden on-chain by the admin via `set_max_plans_per_merchant`. +const MAX_PLANS_PER_MERCHANT: u32 = 100; + const STORAGE_VERSION: u32 = 2; #[soroban_sdk::contracttype] @@ -352,11 +357,11 @@ fn resolve_charge_price(env: &Env, storage: &Address, plan: &Plan) -> i128 { } let token_sym = token_sym_opt.unwrap(); - let quote_sym = Symbol::new(env, &string_to_symbol_str(env, &bounds.quote)); + let quote_sym = bounds.quote; let client = SubTrackrOracleClient::new(env, &oracle); - if let Ok(price) = client.try_get_price_with_cache(&token_sym, "e_sym, &600) { + if let Ok(Ok(price)) = client.try_get_price_with_cache(&token_sym, "e_sym, &600) { let oracle_value = price.value; if oracle_value <= 0 { return plan.price; @@ -377,15 +382,6 @@ fn resolve_charge_price(env: &Env, storage: &Address, plan: &Plan) -> i128 { } } -fn string_to_symbol_str(env: &Env, s: &String) -> soroban_sdk::Vec { - let bytes = s.as_bytes(); - let mut result: soroban_sdk::Vec = soroban_sdk::Vec::new(env); - for i in 0..bytes.len() { - result.push_back(bytes.get(i).unwrap()); - } - result -} - // ───────────────────────────────────────────────────────────────────────────── // Implementation Contract // ───────────────────────────────────────────────────────────────────────────── @@ -644,6 +640,28 @@ impl SubTrackrSubscription { storage_instance_remove(&env, &storage, StorageKey::RateLimit(function)); } + // ── Plan Limit Admin ── + + pub fn set_max_plans_per_merchant( + env: Env, + proxy: Address, + storage: Address, + new_limit: u32, + ) { + proxy.require_auth(); + let admin = get_admin(&env, &storage); + require_permission(&env, &storage, &admin, Permission::SetPlanQuotas); + admin.require_auth(); + assert!(new_limit > 0, "Max plans per merchant must be > 0"); + storage_instance_set(&env, &storage, StorageKey::MaxPlansPerMerchant, new_limit); + } + + pub fn get_max_plans_per_merchant(env: Env, proxy: Address, storage: Address) -> u32 { + proxy.require_auth(); + storage_instance_get(&env, &storage, StorageKey::MaxPlansPerMerchant) + .unwrap_or(MAX_PLANS_PER_MERCHANT) + } + // ── Plan Management ── pub fn create_plan( @@ -663,8 +681,20 @@ impl SubTrackrSubscription { merchant.require_auth(); assert!(price > 0, "Price must be positive"); + let max_plans: u32 = + storage_instance_get(&env, &storage, StorageKey::MaxPlansPerMerchant) + .unwrap_or(MAX_PLANS_PER_MERCHANT); + assert!(max_plans > 0, "Max plans per merchant must be > 0"); + let mut count: u64 = storage_instance_get(&env, &storage, StorageKey::PlanCount).unwrap_or(0); + + let mut merchant_plans: Vec = + storage_persistent_get(&env, &storage, StorageKey::MerchantPlans(merchant.clone())) + .unwrap_or(Vec::new(&env)); + if merchant_plans.len() >= max_plans { + panic!("Max plans per merchant reached"); + } count += 1; let plan = Plan { @@ -682,9 +712,6 @@ impl SubTrackrSubscription { 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, @@ -990,7 +1017,7 @@ impl SubTrackrSubscription { let plan: Plan = storage_persistent_get(&env, &storage, StorageKey::Plan(sub.plan_id)) .expect("Plan not found"); - let charge_price = Self::resolve_charge_price(&env, &storage, &plan); + let charge_price = resolve_charge_price(&env, &storage, &plan); token::Client::new(&env, &plan.token).transfer( &sub.subscriber, @@ -1324,6 +1351,15 @@ impl SubTrackrSubscription { storage_instance_get(&env, &storage, StorageKey::SubscriptionCount).unwrap_or(0) } +} + +// ── Extended APIs (disabled by default) ── +// +// These APIs depend on additional modules/types that are still evolving. +// Enable with `--features extended` in the `subtrackr-subscription` crate. +#[cfg(feature = "extended")] +#[soroban_sdk::contractimpl] +impl SubTrackrSubscription { // ── Revenue Recognition API ── /// Set a revenue recognition rule for a plan (merchant only). @@ -1626,6 +1662,7 @@ impl SubTrackrSubscription { // Proration & Plan Changes /// Preview proration before confirming a plan change +#[cfg(feature = "extended")] pub fn preview_proration( env: Env, proxy: Address, @@ -1655,6 +1692,7 @@ pub fn preview_proration( } /// Execute a plan change with proration +#[cfg(feature = "extended")] pub fn change_plan( env: Env, proxy: Address, @@ -1775,6 +1813,7 @@ pub fn change_plan( } /// Get stored credit memo for a subscription +#[cfg(feature = "extended")] pub fn get_credit_memo( env: Env, proxy: Address, @@ -1786,6 +1825,7 @@ pub fn get_credit_memo( } /// Apply credit memo to next charge +#[cfg(feature = "extended")] pub fn apply_credit_memo_to_charge( env: Env, proxy: Address, diff --git a/contracts/types/src/lib.rs b/contracts/types/src/lib.rs index 38b872eb..96c25cc5 100644 --- a/contracts/types/src/lib.rs +++ b/contracts/types/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] -use soroban_sdk::{contracttype, Address, String, Vec}; +use soroban_sdk::{contracttype, Address, String, Symbol, Vec}; /// Billing interval in seconds. #[contracttype] @@ -555,6 +555,13 @@ pub enum DigitalGoodsClass { TelecomService, } +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum MaybeDigitalGoodsClass { + None, + Some(DigitalGoodsClass), +} + /// A tax rate entry for a specific jurisdiction and tax type. #[contracttype] #[derive(Clone, Debug, PartialEq)] @@ -579,7 +586,7 @@ pub struct CustomerTaxStatus { pub certificate_expiry: Timestamp, pub issuing_authority: String, pub exempt_jurisdictions: Vec, - pub digital_goods_override: Option, + pub digital_goods_override: MaybeDigitalGoodsClass, } /// A single line in a tax remittance report recording collected tax by jurisdiction. @@ -700,6 +707,11 @@ pub enum StorageKey { /// Temporary nonce used to deduplicate rapid charge attempts within a /// single ledger sequence window. Expires after one ledger close (~5 s). TmpChargeNonce(u64), + + // ── Added in storage version 7 (Plan limits) ── + /// Global maximum number of plans a merchant can create. + /// Stored in instance storage; if unset, the implementation default applies. + MaxPlansPerMerchant, } /// Slippage protection bounds for oracle-based pricing. @@ -711,5 +723,5 @@ pub struct PriceBounds { /// Minimum allowed price as basis points of the stored plan price (e.g. 9500 = -5%). pub min_price_bps: u32, /// Quote currency symbol used for price lookup (e.g. "USD"). - pub quote: String, + pub quote: Symbol, } From c63b57bed1bc493184b8f3942ccbc5719b7ede35 Mon Sep 17 00:00:00 2001 From: manueldezman Date: Mon, 1 Jun 2026 15:22:28 +0100 Subject: [PATCH 2/3] chore(contracts): fix clippy warnings --- contracts/access_control/src/lib.rs | 6 +++--- contracts/batch/src/lib.rs | 2 +- contracts/fraud/src/lib.rs | 4 ++-- contracts/metering/src/lib.rs | 1 + contracts/oracle/src/lib.rs | 1 + .../storage/src/transient_storage_tests.rs | 3 +-- .../subscription/src/gas_optimization.rs | 20 +++++++++---------- contracts/subscription/src/gas_profiler.rs | 9 +++++---- contracts/subscription/src/gas_storage.rs | 17 ++++++++-------- contracts/subscription/src/lib.rs | 2 ++ contracts/subscription/src/revenue.rs | 1 + 11 files changed, 34 insertions(+), 32 deletions(-) diff --git a/contracts/access_control/src/lib.rs b/contracts/access_control/src/lib.rs index ca988036..da0658e7 100644 --- a/contracts/access_control/src/lib.rs +++ b/contracts/access_control/src/lib.rs @@ -206,7 +206,7 @@ impl RoleManager { .unwrap_or(Vec::new(&env)); let mut new_members: Vec
= Vec::new(&env); for m in members.iter() { - if &m != &user { + if m != user { new_members.push_back(m); } } @@ -221,7 +221,7 @@ impl RoleManager { .unwrap_or(Vec::new(&env)); let mut new_roles: Vec = Vec::new(&env); for r in user_roles.iter() { - if &r != &role { + if r != role { new_roles.push_back(r); } } @@ -600,7 +600,7 @@ impl RoleManager { .unwrap_or(DEFAULT_MULTISIG_THRESHOLD); assert!( - proposal.approvals.len() >= threshold as u32, + proposal.approvals.len() >= threshold, "Insufficient approvals" ); diff --git a/contracts/batch/src/lib.rs b/contracts/batch/src/lib.rs index 2e34150e..74e2ad4a 100644 --- a/contracts/batch/src/lib.rs +++ b/contracts/batch/src/lib.rs @@ -200,7 +200,7 @@ impl SubTrackrBatch { .get(&DataKey::BatchAtomic(batch_id)) .unwrap_or(false); - let total = op.subscription_ids.len() as u32; + let total = op.subscription_ids.len(); let gas_estimate = estimate_batch_gas(&op); let mut successful: u32 = 0; diff --git a/contracts/fraud/src/lib.rs b/contracts/fraud/src/lib.rs index 125c6892..f3b19848 100644 --- a/contracts/fraud/src/lib.rs +++ b/contracts/fraud/src/lib.rs @@ -325,7 +325,7 @@ impl SubTrackrFraud { pub fn assess_risk(env: Env, subscriber: Address) -> RiskScore { let ids = get_subscriptions(&env, &subscriber); - if ids.len() == 0 { + if ids.is_empty() { return RiskScore { subscriber: subscriber.clone(), subscription_id: 0, @@ -452,7 +452,7 @@ impl SubTrackrFraud { i += 1; } - let average_risk = if ids.len() == 0 { + let average_risk = if ids.is_empty() { 0 } else { total_risk / ids.len() diff --git a/contracts/metering/src/lib.rs b/contracts/metering/src/lib.rs index 4131d014..6a566c47 100644 --- a/contracts/metering/src/lib.rs +++ b/contracts/metering/src/lib.rs @@ -1,4 +1,5 @@ #![no_std] +#![allow(clippy::too_many_arguments)] //! SubTrackr usage-metering contract. //! //! Real-time usage-based billing (issue: metered billing). Reporters push diff --git a/contracts/oracle/src/lib.rs b/contracts/oracle/src/lib.rs index 4cc946c2..4e878e6b 100644 --- a/contracts/oracle/src/lib.rs +++ b/contracts/oracle/src/lib.rs @@ -1,4 +1,5 @@ #![no_std] +#![allow(clippy::too_many_arguments)] //! SubTrackr price oracle contract. //! //! A push-style oracle: authorized feed addresses submit signed price diff --git a/contracts/storage/src/transient_storage_tests.rs b/contracts/storage/src/transient_storage_tests.rs index ddf18d65..6e425e13 100644 --- a/contracts/storage/src/transient_storage_tests.rs +++ b/contracts/storage/src/transient_storage_tests.rs @@ -8,9 +8,8 @@ /// /// Run with: /// cargo test -p subtrackr-storage -- transient --nocapture - #[cfg(test)] -mod transient_storage_tests { +mod tests { use crate::{SubTrackrStorage, SubTrackrStorageClient}; use soroban_sdk::{ testutils::{Address as _, Ledger, LedgerInfo}, diff --git a/contracts/subscription/src/gas_optimization.rs b/contracts/subscription/src/gas_optimization.rs index ff1a24d7..d20b8996 100644 --- a/contracts/subscription/src/gas_optimization.rs +++ b/contracts/subscription/src/gas_optimization.rs @@ -1,7 +1,8 @@ -/// 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. -use soroban_sdk::{String, Vec, Env}; +use soroban_sdk::{Env, String, Vec}; /// Optimization level #[derive(Clone, Copy)] @@ -13,7 +14,7 @@ pub enum OptimizationLevel { } impl OptimizationLevel { - pub fn to_string(&self) -> &'static str { + pub fn as_str(self) -> &'static str { match self { Self::Critical => "critical", Self::High => "high", @@ -262,17 +263,14 @@ impl GasOptimizations { /// Calculate potential gas savings pub fn calculate_savings(current_gas: u64, target_gas: u64) -> u64 { - if current_gas > target_gas { - current_gas - target_gas - } else { - 0 - } + current_gas.saturating_sub(target_gas) } } - pub fn get_optimization_priorities( +#[allow(dead_code)] +pub fn get_optimization_priorities( env: &Env, - gas_metrics: Vec<(String, u64)>, + _gas_metrics: Vec<(String, u64)>, ) -> Vec<(String, u64, String)> { Vec::new(env) } diff --git a/contracts/subscription/src/gas_profiler.rs b/contracts/subscription/src/gas_profiler.rs index efb554b5..6ffb16e8 100644 --- a/contracts/subscription/src/gas_profiler.rs +++ b/contracts/subscription/src/gas_profiler.rs @@ -1,7 +1,8 @@ -/// 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}; /// 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 85c4e2e8..68b1c46d 100644 --- a/contracts/subscription/src/gas_storage.rs +++ b/contracts/subscription/src/gas_storage.rs @@ -1,7 +1,8 @@ -/// Gas Storage Module -/// Manages storage and retrieval of gas profiling metrics - -use soroban_sdk::{Address, Env, String as SorobanString, TryFromVal, Val, IntoVal, 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}; /// Storage keys for gas metrics @@ -189,11 +190,9 @@ impl GasMetricsStorage { pub fn get_metrics_summary(env: &Env, storage: &Address) -> (u64, u64, u64) { let total_gas = Self::get_total_gas_used(env, storage); let total_calls = Self::get_total_call_count(env, storage); - let avg_gas = if total_calls > 0 { - total_gas / total_calls - } else { - 0 - }; + let avg_gas = total_gas + .checked_div(total_calls) + .unwrap_or(0); (total_gas, total_calls, avg_gas) } } diff --git a/contracts/subscription/src/lib.rs b/contracts/subscription/src/lib.rs index 4a2ce79d..c4cccbe9 100644 --- a/contracts/subscription/src/lib.rs +++ b/contracts/subscription/src/lib.rs @@ -1,4 +1,5 @@ #![no_std] +#![allow(clippy::too_many_arguments)] mod gas_optimization; mod gas_profiler; mod gas_storage; @@ -192,6 +193,7 @@ fn storage_temporary_set>( ); } +#[allow(dead_code)] fn storage_temporary_remove(env: &Env, storage: &Address, key: StorageKey) { let args: Vec = soroban_sdk::vec![env, key.into_val(env)]; env.invoke_contract::<()>( diff --git a/contracts/subscription/src/revenue.rs b/contracts/subscription/src/revenue.rs index c33c8844..2688986f 100644 --- a/contracts/subscription/src/revenue.rs +++ b/contracts/subscription/src/revenue.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] /// Revenue recognition module for SubTrackr subscriptions. /// /// Implements ASC 606 / IFRS 15 compliant revenue recognition: From 4328de965845e779af909553e7a801aa719266e4 Mon Sep 17 00:00:00 2001 From: manueldezman Date: Mon, 1 Jun 2026 16:07:48 +0100 Subject: [PATCH 3/3] chore: lint/format cleanup --- App.tsx | 2 - developer-portal/components/ApiKeyManager.tsx | 54 ++--- .../components/DeveloperOnboarding.tsx | 23 +- developer-portal/pages/ApiKeysPage.tsx | 84 ++----- developer-portal/pages/DashboardPage.tsx | 73 ++---- developer-portal/pages/DocumentationPage.tsx | 41 +--- developer-portal/pages/OnboardingPage.tsx | 113 +++------ developer-portal/pages/UsagePage.tsx | 65 ++---- .../services/developerPortalService.ts | 156 ++++++++++--- .../services/documentationService.ts | 14 +- .../services/integrationGuidesService.ts | 8 +- developer-portal/services/portalService.ts | 8 +- .../src/components/DashboardCard.tsx | 4 +- .../src/components/PermissionSelector.tsx | 3 +- .../src/components/QuickActionCard.tsx | 2 +- .../src/screens/ApiKeyManagementScreen.tsx | 26 +-- .../src/screens/ApiTesterScreen.tsx | 23 +- .../src/screens/DeveloperPortalScreen.tsx | 56 ++--- .../src/screens/SdkDownloadScreen.tsx | 2 +- .../src/screens/UsageAnalyticsScreen.tsx | 8 +- .../src/screens/WebhookTesterScreen.tsx | 16 +- developer-portal/types/developer.ts | 7 +- developer-portal/types/portal.ts | 7 +- .../utils/developerPortalUtils.ts | 46 ++-- sandbox/__tests__/developerPortal.test.ts | 46 +--- sandbox/__tests__/sandbox.test.ts | 24 +- sandbox/api/sandboxApi.ts | 11 +- sandbox/config/sandboxConfig.ts | 9 +- sandbox/middleware/sandboxMiddleware.ts | 46 +--- sandbox/services/apiKeyService.ts | 6 +- sandbox/services/sandboxIsolationService.ts | 44 ++-- sandbox/services/sandboxService.ts | 82 +++---- sandbox/services/usageTrackingService.ts | 59 ++--- sandbox/utils/sandboxUtils.ts | 41 +++- sandbox/utils/testDataGenerator.ts | 37 ++- sdks/javascript/src/errors.ts | 6 +- src/animations/index.ts | 6 +- src/components/UsageDashboard.tsx | 2 +- src/components/common/ScreenTemplates.tsx | 10 +- src/components/common/SwipeableCard.tsx | 6 +- .../developer/DeveloperComponents.tsx | 4 +- src/components/home/StatsCard.tsx | 5 +- .../subscription/AnimatedSubscriptionCard.tsx | 1 - .../subscription/SubscriptionCard.tsx | 3 - src/config/features.ts | 3 +- src/navigation/AppNavigator.tsx | 2 +- src/screens/AffiliateDashboardScreen.tsx | 165 +++++--------- src/screens/AnalyticsScreen.tsx | 7 - src/screens/ApiKeyManagementScreen.tsx | 23 +- src/screens/CalendarIntegrationScreen.tsx | 58 +++-- src/screens/CampaignManagementScreen.tsx | 121 ++++------ src/screens/CancellationFlowScreen.tsx | 4 +- src/screens/DeveloperPortalScreen.tsx | 50 ++-- src/screens/DocumentationPortalScreen.tsx | 40 ++-- src/screens/FraudDashboard.tsx | 84 +++++-- src/screens/GroupManagementScreen.tsx | 4 +- src/screens/HomeScreen.tsx | 3 - src/screens/IntegrationGuideDetailScreen.tsx | 21 +- src/screens/IntegrationGuidesScreen.tsx | 29 +-- src/screens/LoyaltyDashboardScreen.tsx | 157 ++++++------- src/screens/MerchantOnboardingScreen.tsx | 37 +-- src/screens/RoleManagementScreen.tsx | 118 ++++++---- src/screens/SandboxDashboardScreen.tsx | 73 +++--- src/screens/SandboxDetailScreen.tsx | 47 ++-- src/screens/SandboxScreen.tsx | 109 +++------ src/screens/SupportDashboardScreen.tsx | 17 +- src/screens/TaxSettingsScreen.tsx | 6 +- src/services/FEATURE_GATING_README.md | 63 ++++- src/services/__tests__/slaService.test.ts | 23 +- src/services/__tests__/walletService.test.ts | 16 +- src/services/accountingExport.ts | 8 +- src/services/analyticsService.ts | 20 +- src/services/calendarService.ts | 16 +- src/services/groupService.ts | 11 +- src/services/notificationService.ts | 12 +- src/services/oraclePriceService.ts | 22 +- .../sandbox/developerOnboardingService.ts | 6 +- .../sandbox/developerPortalService.ts | 8 +- src/services/sandbox/documentationService.ts | 6 +- src/services/sandbox/sandboxService.ts | 67 ++++-- src/services/sandbox/testDataGenerator.ts | 76 +++++-- src/services/taxService.ts | 9 +- src/services/walletService.ts | 51 +++-- src/store/__tests__/slaStore.test.ts | 9 +- src/store/affiliateStore.ts | 20 +- src/store/calendarStore.ts | 8 +- src/store/developerPortalStore.ts | 37 +-- src/store/fraudStore.ts | 156 ++++++++++--- src/store/groupStore.ts | 7 +- src/store/invoiceStore.ts | 60 +++-- src/store/loyaltyStore.ts | 2 +- src/store/merchantStore.ts | 6 +- src/store/sandboxStore.ts | 215 ++++++++++++++---- src/store/settingsStore.ts | 4 +- src/store/subscriptionStore.ts | 85 +++---- src/store/supportStore.ts | 8 +- src/store/taxStore.ts | 8 +- src/store/walletStore.ts | 7 +- src/types/affiliate.ts | 2 +- src/types/calendar.ts | 2 +- src/types/developerPortal.ts | 24 +- src/types/fraud.ts | 7 +- src/types/invoice.ts | 26 ++- src/types/loyalty.ts | 2 +- src/types/merchant.ts | 2 +- src/types/rateLimiting.ts | 5 +- src/types/sandbox.ts | 14 +- src/utils/formatting.ts | 1 - src/utils/invoice.ts | 10 +- src/utils/proration.ts | 54 ++--- tsconfig.json | 4 +- 111 files changed, 1876 insertions(+), 1790 deletions(-) diff --git a/App.tsx b/App.tsx index f08cd6bf..6ff13d28 100644 --- a/App.tsx +++ b/App.tsx @@ -19,7 +19,6 @@ import { EVM_RPC_URLS } from './src/config/evm'; import { useNetworkStore, useSettingsStore } from './src/store'; import { sessionService } from './src/services/auth/session'; - // Get projectId from environment variable const projectId = process.env.WALLET_CONNECT_PROJECT_ID || 'YOUR_PROJECT_ID'; @@ -85,7 +84,6 @@ function NotificationBootstrap() { void sessionService.initializeCurrentSession(); }, [initialize, initializeSettings]); - return null; } diff --git a/developer-portal/components/ApiKeyManager.tsx b/developer-portal/components/ApiKeyManager.tsx index c005a13c..aed22d54 100644 --- a/developer-portal/components/ApiKeyManager.tsx +++ b/developer-portal/components/ApiKeyManager.tsx @@ -38,7 +38,7 @@ const AVAILABLE_PERMISSIONS = [ ]; export const ApiKeyManager: React.FC = ({ - environmentId, + environmentId: _environmentId, apiKeys, onCreateKey, onRevokeKey, @@ -115,10 +115,8 @@ export const ApiKeyManager: React.FC = ({ }; const togglePermission = (permission: string) => { - setSelectedPermissions(prev => - prev.includes(permission) - ? prev.filter(p => p !== permission) - : [...prev, permission] + setSelectedPermissions((prev) => + prev.includes(permission) ? prev.filter((p) => p !== permission) : [...prev, permission] ); }; @@ -135,14 +133,13 @@ export const ApiKeyManager: React.FC = ({ item.status === 'active' && styles.statusActive, item.status === 'revoked' && styles.statusRevoked, item.status === 'expired' && styles.statusExpired, - ]} - > + ]}> {item.status} - {item.permissions.map(permission => ( + {item.permissions.map((permission) => ( {permission} @@ -150,13 +147,9 @@ export const ApiKeyManager: React.FC = ({ - - Created: {item.createdAt.toLocaleDateString()} - + Created: {item.createdAt.toLocaleDateString()} {item.lastUsedAt && ( - - Last used: {item.lastUsedAt.toLocaleDateString()} - + Last used: {item.lastUsedAt.toLocaleDateString()} )} @@ -164,14 +157,12 @@ export const ApiKeyManager: React.FC = ({ handleRotateKey(item.id, item.name)} - > + onPress={() => handleRotateKey(item.id, item.name)}> Rotate handleRevokeKey(item.id, item.name)} - > + onPress={() => handleRevokeKey(item.id, item.name)}> Revoke @@ -183,10 +174,7 @@ export const ApiKeyManager: React.FC = ({ API Keys - setModalVisible(true)} - > + setModalVisible(true)}> + Create Key @@ -194,7 +182,7 @@ export const ApiKeyManager: React.FC = ({ item.id} + keyExtractor={(item) => item.id} contentContainerStyle={styles.listContainer} ListEmptyComponent={ @@ -210,8 +198,7 @@ export const ApiKeyManager: React.FC = ({ visible={modalVisible} animationType="slide" transparent={true} - onRequestClose={() => setModalVisible(false)} - > + onRequestClose={() => setModalVisible(false)}> Create API Key @@ -227,23 +214,20 @@ export const ApiKeyManager: React.FC = ({ Permissions - {AVAILABLE_PERMISSIONS.map(permission => ( + {AVAILABLE_PERMISSIONS.map((permission) => ( togglePermission(permission)} - > + onPress={() => togglePermission(permission)}> + ]}> {permission} @@ -253,15 +237,13 @@ export const ApiKeyManager: React.FC = ({ setModalVisible(false)} - > + onPress={() => setModalVisible(false)}> Cancel + disabled={loading}> {loading ? 'Creating...' : 'Create Key'} diff --git a/developer-portal/components/DeveloperOnboarding.tsx b/developer-portal/components/DeveloperOnboarding.tsx index 1ea9e6c6..770a06a5 100644 --- a/developer-portal/components/DeveloperOnboarding.tsx +++ b/developer-portal/components/DeveloperOnboarding.tsx @@ -42,7 +42,7 @@ export const DeveloperOnboarding: React.FC = ({ [onStepComplete] ); - const allCompleted = steps.every(step => step.completed); + const allCompleted = steps.every((step) => step.completed); return ( @@ -59,13 +59,13 @@ export const DeveloperOnboarding: React.FC = ({ style={[ styles.progressFill, { - width: `${(steps.filter(s => s.completed).length / steps.length) * 100}%`, + width: `${(steps.filter((s) => s.completed).length / steps.length) * 100}%`, }, ]} /> - {steps.filter(s => s.completed).length} of {steps.length} completed + {steps.filter((s) => s.completed).length} of {steps.length} completed @@ -79,15 +79,9 @@ export const DeveloperOnboarding: React.FC = ({ index === currentStep && styles.stepCardActive, ]} onPress={() => !step.completed && handleStepPress(step.id)} - disabled={step.completed || loading} - > + disabled={step.completed || loading}> - + {step.completed ? ( ) : ( @@ -95,12 +89,7 @@ export const DeveloperOnboarding: React.FC = ({ )} - + {step.title} {step.description} diff --git a/developer-portal/pages/ApiKeysPage.tsx b/developer-portal/pages/ApiKeysPage.tsx index 1a859c93..428a925a 100644 --- a/developer-portal/pages/ApiKeysPage.tsx +++ b/developer-portal/pages/ApiKeysPage.tsx @@ -35,7 +35,7 @@ const AVAILABLE_PERMISSIONS = [ { id: 'analytics', label: 'Analytics', description: 'Access usage analytics' }, ]; -export const ApiKeysPage: React.FC = ({ environmentId }) => { +export const ApiKeysPage: React.FC = ({ environmentId: _environmentId }) => { const [apiKeys, setApiKeys] = useState([]); const [modalVisible, setModalVisible] = useState(false); const [newKeyName, setNewKeyName] = useState(''); @@ -106,9 +106,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { status: 'active', lastUsedAt: null, createdAt: new Date(), - expiresAt: new Date( - Date.now() + parseInt(expirationDays) * 86400000 - ), + expiresAt: new Date(Date.now() + parseInt(expirationDays) * 86400000), }; setApiKeys((prev) => [...prev, key]); @@ -135,9 +133,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { style: 'destructive', onPress: () => { setApiKeys((prev) => - prev.map((k) => - k.id === keyId ? { ...k, status: 'revoked' as const } : k - ) + prev.map((k) => (k.id === keyId ? { ...k, status: 'revoked' as const } : k)) ); }, }, @@ -156,11 +152,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { onPress: () => { const newKey = `sk_test_${Math.random().toString(36).substring(2, 38)}`; setApiKeys((prev) => - prev.map((k) => - k.id === keyId - ? { ...k, key: newKey, lastUsedAt: null } - : k - ) + prev.map((k) => (k.id === keyId ? { ...k, key: newKey, lastUsedAt: null } : k)) ); setGeneratedKey(newKey); setShowKeyModal(true); @@ -177,13 +169,11 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { const togglePermission = (permission: string) => { setSelectedPermissions((prev) => - prev.includes(permission) - ? prev.filter((p) => p !== permission) - : [...prev, permission] + prev.includes(permission) ? prev.filter((p) => p !== permission) : [...prev, permission] ); }; - const copyToClipboard = (text: string) => { + const copyToClipboard = (_text: string) => { Alert.alert('Copied', 'API key copied to clipboard'); }; @@ -202,8 +192,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { item.status === 'active' && styles.statusActive, item.status === 'revoked' && styles.statusRevoked, item.status === 'expired' && styles.statusExpired, - ]} - > + ]}> {item.status} @@ -217,18 +206,12 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { - - Created: {item.createdAt.toLocaleDateString()} - + Created: {item.createdAt.toLocaleDateString()} {item.lastUsedAt && ( - - Last used: {item.lastUsedAt.toLocaleDateString()} - + Last used: {item.lastUsedAt.toLocaleDateString()} )} {item.expiresAt && ( - - Expires: {item.expiresAt.toLocaleDateString()} - + Expires: {item.expiresAt.toLocaleDateString()} )} @@ -236,14 +219,12 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { handleRotateKey(item.id, item.name)} - > + onPress={() => handleRotateKey(item.id, item.name)}> Rotate handleRevokeKey(item.id, item.name)} - > + onPress={() => handleRevokeKey(item.id, item.name)}> Revoke @@ -256,14 +237,9 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { API Keys - - Manage your API keys for sandbox access - + Manage your API keys for sandbox access - setModalVisible(true)} - > + setModalVisible(true)}> + Create Key @@ -271,8 +247,8 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { API Key Security - Keep your API keys secure. Never share them in public repositories or - client-side code. Use environment variables for production keys. + Keep your API keys secure. Never share them in public repositories or client-side code. + Use environment variables for production keys. @@ -296,8 +272,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { visible={modalVisible} animationType="slide" transparent={true} - onRequestClose={() => setModalVisible(false)} - > + onRequestClose={() => setModalVisible(false)}> @@ -312,9 +287,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { placeholderTextColor="#9CA3AF" /> - - Expiration (days) - + Expiration (days) = ({ environmentId }) => { selectedPermissions.includes(permission.id) && styles.permissionOptionSelected, ]} - onPress={() => togglePermission(permission.id)} - > + onPress={() => togglePermission(permission.id)}> + ]}> {permission.label} @@ -360,15 +331,13 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { setModalVisible(false)} - > + onPress={() => setModalVisible(false)}> Cancel + disabled={loading}> {loading ? 'Creating...' : 'Create Key'} @@ -383,14 +352,12 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { visible={showKeyModal} animationType="slide" transparent={true} - onRequestClose={() => setShowKeyModal(false)} - > + onRequestClose={() => setShowKeyModal(false)}> API Key Created - Your API key has been created. Copy it now - you won't be able to - see it again! + Your API key has been created. Copy it now - you won't be able to see it again! @@ -402,8 +369,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { onPress={() => { copyToClipboard(generatedKey); setShowKeyModal(false); - }} - > + }}> Copy & Close diff --git a/developer-portal/pages/DashboardPage.tsx b/developer-portal/pages/DashboardPage.tsx index 1b4b3d45..86b5126a 100644 --- a/developer-portal/pages/DashboardPage.tsx +++ b/developer-portal/pages/DashboardPage.tsx @@ -1,12 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { - View, - Text, - StyleSheet, - ScrollView, - TouchableOpacity, - RefreshControl, -} from 'react-native'; +import { View, Text, StyleSheet, ScrollView, TouchableOpacity, RefreshControl } from 'react-native'; interface DashboardStats { totalRequests: number; @@ -135,22 +128,15 @@ export const DashboardPage: React.FC = ({ onNavigate }) => { return ( - } - > + refreshControl={}> Developer Dashboard - - Manage your sandbox environments and API integrations - + Manage your sandbox environments and API integrations - - {stats.totalRequests.toLocaleString()} - + {stats.totalRequests.toLocaleString()} Total Requests @@ -174,31 +160,21 @@ export const DashboardPage: React.FC = ({ onNavigate }) => { Quick Actions - onNavigate('api-keys')} - > + onNavigate('api-keys')}> 🔑 Create API Key onNavigate('environments')} - > + onPress={() => onNavigate('environments')}> 🌍 New Environment - onNavigate('docs')} - > + onNavigate('docs')}> 📚 View Docs - onNavigate('usage')} - > + onNavigate('usage')}> 📊 Usage Stats @@ -220,28 +196,15 @@ export const DashboardPage: React.FC = ({ onNavigate }) => { style={[ styles.statusBadge, { backgroundColor: getStatusColor(env.status) + '20' }, - ]} - > - - + ]}> + + {env.status} - - {env.requestCount.toLocaleString()} requests - + {env.requestCount.toLocaleString()} requests {env.errorRate}% errors @@ -254,16 +217,10 @@ export const DashboardPage: React.FC = ({ onNavigate }) => { {recentActivity.map((activity) => ( - - {getActivityIcon(activity.type)} - + {getActivityIcon(activity.type)} - - {activity.description} - - - {activity.timestamp.toLocaleString()} - + {activity.description} + {activity.timestamp.toLocaleString()} ))} diff --git a/developer-portal/pages/DocumentationPage.tsx b/developer-portal/pages/DocumentationPage.tsx index 3830e159..ebb81a86 100644 --- a/developer-portal/pages/DocumentationPage.tsx +++ b/developer-portal/pages/DocumentationPage.tsx @@ -146,9 +146,7 @@ const QUICK_START_GUIDES: QuickStartGuide[] = [ export const DocumentationPage: React.FC = () => { const [searchQuery, setSearchQuery] = useState(''); - const [selectedSection, setSelectedSection] = useState( - null - ); + const [selectedSection, setSelectedSection] = useState(null); const filteredSections = DOC_SECTIONS.filter( (section) => @@ -171,26 +169,14 @@ export const DocumentationPage: React.FC = () => { const renderSection = ({ item }: { item: DocSection }) => ( - setSelectedSection( - selectedSection?.id === item.id ? null : item - ) - } - > + style={[styles.sectionCard, selectedSection?.id === item.id && styles.sectionCardSelected]} + onPress={() => setSelectedSection(selectedSection?.id === item.id ? null : item)}> {item.icon} {item.title} - - {selectedSection?.id === item.id ? '▼' : '▶'} - + {selectedSection?.id === item.id ? '▼' : '▶'} - {selectedSection?.id === item.id && ( - {item.content} - )} + {selectedSection?.id === item.id && {item.content}} ); @@ -202,14 +188,8 @@ export const DocumentationPage: React.FC = () => { style={[ styles.difficultyBadge, { backgroundColor: getDifficultyColor(item.difficulty) + '20' }, - ]} - > - + ]}> + {item.difficulty} @@ -223,9 +203,7 @@ export const DocumentationPage: React.FC = () => { Documentation - - Everything you need to integrate with SubTrackr - + Everything you need to integrate with SubTrackr @@ -261,8 +239,7 @@ export const DocumentationPage: React.FC = () => { Need Help? - Can't find what you're looking for? Contact our developer support - team. + Can't find what you're looking for? Contact our developer support team. Contact Support diff --git a/developer-portal/pages/OnboardingPage.tsx b/developer-portal/pages/OnboardingPage.tsx index 7db44fc6..ba355fcd 100644 --- a/developer-portal/pages/OnboardingPage.tsx +++ b/developer-portal/pages/OnboardingPage.tsx @@ -21,9 +21,7 @@ interface OnboardingPageProps { onComplete: () => void; } -export const OnboardingPage: React.FC = ({ - onComplete, -}) => { +export const OnboardingPage: React.FC = ({ onComplete }) => { const [currentStep, setCurrentStep] = useState(0); const [steps, setSteps] = useState([ { @@ -77,19 +75,12 @@ export const OnboardingPage: React.FC = ({ }); const completedCount = steps.filter((s) => s.completed).length; - const requiredCount = steps.filter((s) => s.isRequired).length; - const allRequiredCompleted = steps - .filter((s) => s.isRequired) - .every((s) => s.completed); + const allRequiredCompleted = steps.filter((s) => s.isRequired).every((s) => s.completed); const handleCompleteStep = (stepId: string) => { - setSteps((prev) => - prev.map((s) => (s.id === stepId ? { ...s, completed: true } : s)) - ); + setSteps((prev) => prev.map((s) => (s.id === stepId ? { ...s, completed: true } : s))); - const nextStep = steps.findIndex( - (s) => !s.completed && s.id !== stepId - ); + const nextStep = steps.findIndex((s) => !s.completed && s.id !== stepId); if (nextStep !== -1) { setCurrentStep(nextStep); } @@ -105,11 +96,9 @@ export const OnboardingPage: React.FC = ({ }; const handleCreateSandbox = () => { - Alert.alert( - 'Sandbox Created', - 'Your sandbox environment has been created successfully!', - [{ text: 'OK', onPress: () => handleCompleteStep('create-sandbox') }] - ); + Alert.alert('Sandbox Created', 'Your sandbox environment has been created successfully!', [ + { text: 'OK', onPress: () => handleCompleteStep('create-sandbox') }, + ]); }; const handleGenerateApiKey = () => { @@ -138,9 +127,7 @@ export const OnboardingPage: React.FC = ({ - setFormData((prev) => ({ ...prev, name: text })) - } + onChangeText={(text) => setFormData((prev) => ({ ...prev, name: text }))} placeholder="John Doe" placeholderTextColor="#9CA3AF" /> @@ -148,9 +135,7 @@ export const OnboardingPage: React.FC = ({ - setFormData((prev) => ({ ...prev, email: text })) - } + onChangeText={(text) => setFormData((prev) => ({ ...prev, email: text }))} placeholder="john@example.com" placeholderTextColor="#9CA3AF" keyboardType="email-address" @@ -159,16 +144,11 @@ export const OnboardingPage: React.FC = ({ - setFormData((prev) => ({ ...prev, company: text })) - } + onChangeText={(text) => setFormData((prev) => ({ ...prev, company: text }))} placeholder="Acme Inc" placeholderTextColor="#9CA3AF" /> - + Create Account @@ -178,16 +158,11 @@ export const OnboardingPage: React.FC = ({ return ( - Your sandbox environment will be created with default settings. - You can customize it later in the environment settings. + Your sandbox environment will be created with default settings. You can customize it + later in the environment settings. - - - Create Sandbox Environment - + + Create Sandbox Environment ); @@ -196,13 +171,10 @@ export const OnboardingPage: React.FC = ({ return ( - Generate an API key to authenticate your requests. Keep this key - secure and never share it publicly. + Generate an API key to authenticate your requests. Keep this key secure and never + share it publicly. - + Generate API Key @@ -212,16 +184,13 @@ export const OnboardingPage: React.FC = ({ return ( - Review the API documentation to understand available endpoints, - authentication, and best practices. + Review the API documentation to understand available endpoints, authentication, and + best practices. handleCompleteStep('explore-docs')} - > - - Mark as Reviewed - + onPress={() => handleCompleteStep('explore-docs')}> + Mark as Reviewed ); @@ -233,13 +202,8 @@ export const OnboardingPage: React.FC = ({ {`curl -X GET https://sandbox.api.subtrackr.io/v1/subscriptions \\ -H "Authorization: Bearer sk_test_your_key"`} - - - Test API Call - + + Test API Call ); @@ -261,10 +225,7 @@ export const OnboardingPage: React.FC = ({ @@ -280,19 +241,12 @@ export const OnboardingPage: React.FC = ({ styles.stepCard, step.completed && styles.stepCardCompleted, index === currentStep && styles.stepCardActive, - ]} - > + ]}> !step.completed && setCurrentStep(index)} - disabled={step.completed} - > - + disabled={step.completed}> + {step.completed ? ( ) : ( @@ -300,12 +254,7 @@ export const OnboardingPage: React.FC = ({ )} - + {step.title} {step.description} @@ -316,9 +265,7 @@ export const OnboardingPage: React.FC = ({ {index === currentStep && !step.completed && ( - - {renderStepContent(step)} - + {renderStepContent(step)} )} ))} diff --git a/developer-portal/pages/UsagePage.tsx b/developer-portal/pages/UsagePage.tsx index 96b73e2e..64d8fca8 100644 --- a/developer-portal/pages/UsagePage.tsx +++ b/developer-portal/pages/UsagePage.tsx @@ -1,12 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { - View, - Text, - StyleSheet, - ScrollView, - TouchableOpacity, - RefreshControl, -} from 'react-native'; +import { View, Text, StyleSheet, ScrollView, TouchableOpacity, RefreshControl } from 'react-native'; interface UsageStats { totalRequests: number; @@ -33,13 +26,11 @@ interface UsagePageProps { environmentId?: string; } -export const UsagePage: React.FC = ({ environmentId }) => { +export const UsagePage: React.FC = ({ environmentId: _environmentId }) => { const [stats, setStats] = useState(null); const [topEndpoints, setTopEndpoints] = useState([]); const [hourlyData, setHourlyData] = useState([]); - const [selectedPeriod, setSelectedPeriod] = useState<'24h' | '7d' | '30d'>( - '24h' - ); + const [selectedPeriod, setSelectedPeriod] = useState<'24h' | '7d' | '30d'>('24h'); const [refreshing, setRefreshing] = useState(false); useEffect(() => { @@ -95,33 +86,23 @@ export const UsagePage: React.FC = ({ environmentId }) => { return ( - } - > + refreshControl={}> Usage Analytics - - Monitor your API usage and performance - + Monitor your API usage and performance {(['24h', '7d', '30d'] as const).map((period) => ( setSelectedPeriod(period)} - > + style={[styles.periodButton, selectedPeriod === period && styles.periodButtonActive]} + onPress={() => setSelectedPeriod(period)}> + ]}> {period} @@ -132,9 +113,7 @@ export const UsagePage: React.FC = ({ environmentId }) => { <> - - {stats.totalRequests.toLocaleString()} - + {stats.totalRequests.toLocaleString()} Total Requests @@ -166,15 +145,12 @@ export const UsagePage: React.FC = ({ environmentId }) => { styles.bar, { height: getBarHeight(data.requests, maxRequests), - backgroundColor: - data.errors > 5 ? '#EF4444' : '#3B82F6', + backgroundColor: data.errors > 5 ? '#EF4444' : '#3B82F6', }, ]} /> - {data.hour % 4 === 0 && ( - {data.hour}h - )} + {data.hour % 4 === 0 && {data.hour}h} ))} @@ -187,21 +163,12 @@ export const UsagePage: React.FC = ({ environmentId }) => { #{index + 1} {endpoint.endpoint} - - {endpoint.count.toLocaleString()} - + {endpoint.count.toLocaleString()} - + - - {endpoint.percentage}% of total - + {endpoint.percentage}% of total ))} @@ -223,9 +190,7 @@ export const UsagePage: React.FC = ({ environmentId }) => { Uptime - - 99.95% - + 99.95% diff --git a/developer-portal/services/developerPortalService.ts b/developer-portal/services/developerPortalService.ts index a320b926..53d359ef 100644 --- a/developer-portal/services/developerPortalService.ts +++ b/developer-portal/services/developerPortalService.ts @@ -1,6 +1,5 @@ import { Developer, - OnboardingStatus, ApiKey, ApiPermission, UsageMetrics, @@ -53,7 +52,12 @@ export class DeveloperPortalService { this.alerts.set(developerId, []); await this.logActivity(developerId, 'developer.registered', 'developer', developerId); - await this.createAlert(developerId, 'info', 'Welcome!', 'Your developer account has been created. Please verify your email to continue.'); + await this.createAlert( + developerId, + 'info', + 'Welcome!', + 'Your developer account has been created. Please verify your email to continue.' + ); return developer; } @@ -66,7 +70,10 @@ export class DeveloperPortalService { return false; } - if (developer.onboardingStatus.verificationExpiresAt && developer.onboardingStatus.verificationExpiresAt < new Date()) { + if ( + developer.onboardingStatus.verificationExpiresAt && + developer.onboardingStatus.verificationExpiresAt < new Date() + ) { return false; } @@ -88,17 +95,30 @@ export class DeveloperPortalService { const sandboxEnv = await sandboxService.createEnvironment(developerId, 'Default Sandbox'); developer.onboardingStatus.step = 'completed'; - developer.onboardingStatus.completedSteps.push('profile_completion', 'sandbox_setup', 'completed'); + developer.onboardingStatus.completedSteps.push( + 'profile_completion', + 'sandbox_setup', + 'completed' + ); developer.onboardingStatus.completedAt = new Date(); developer.sandboxEnvironments.push(sandboxEnv.id); developer.updatedAt = new Date(); this.developers.set(developerId, developer); - const apiKey = await this.createApiKey(developerId, 'Default API Key', 'test', ['subscriptions:read', 'subscriptions:write', 'payments:read']); + await this.createApiKey(developerId, 'Default API Key', 'test', [ + 'subscriptions:read', + 'subscriptions:write', + 'payments:read', + ]); await this.logActivity(developerId, 'onboarding.completed', 'developer', developerId); - await this.createAlert(developerId, 'success', 'Onboarding Complete', 'Your developer account is now fully set up. You can start using the API!'); + await this.createAlert( + developerId, + 'success', + 'Onboarding Complete', + 'Your developer account is now fully set up. You can start using the API!' + ); return developer; } @@ -107,7 +127,10 @@ export class DeveloperPortalService { return this.developers.get(developerId) || null; } - async updateDeveloper(developerId: string, updates: Partial): Promise { + async updateDeveloper( + developerId: string, + updates: Partial + ): Promise { const developer = this.developers.get(developerId); if (!developer) return null; @@ -150,7 +173,7 @@ export class DeveloperPortalService { const developer = this.developers.get(developerId); if (!developer) return false; - const apiKey = developer.apiKeys.find(key => key.id === apiKeyId); + const apiKey = developer.apiKeys.find((key) => key.id === apiKeyId); if (!apiKey) return false; apiKey.status = 'revoked'; @@ -168,7 +191,13 @@ export class DeveloperPortalService { return developer.apiKeys; } - async trackUsage(developerId: string, endpoint: string, method: string, responseTime: number, success: boolean): Promise { + async trackUsage( + developerId: string, + endpoint: string, + method: string, + responseTime: number, + success: boolean + ): Promise { const developer = this.developers.get(developerId); if (!developer) return; @@ -180,7 +209,8 @@ export class DeveloperPortalService { } developer.usage.avgResponseTime = - (developer.usage.avgResponseTime * (developer.usage.totalRequests - 1) + responseTime) / developer.usage.totalRequests; + (developer.usage.avgResponseTime * (developer.usage.totalRequests - 1) + responseTime) / + developer.usage.totalRequests; developer.updatedAt = new Date(); this.developers.set(developerId, developer); @@ -197,7 +227,7 @@ export class DeveloperPortalService { if (!developer) return null; const sandboxEnvironments = await Promise.all( - developer.sandboxEnvironments.map(async envId => { + developer.sandboxEnvironments.map(async (envId) => { const env = await sandboxService.getEnvironment(envId); const metrics = await sandboxService.getMetrics(envId); return { @@ -217,7 +247,7 @@ export class DeveloperPortalService { developer, sandboxEnvironments, recentActivity: recentActivity.slice(-10), - alerts: alerts.filter(a => !a.isRead).slice(-5), + alerts: alerts.filter((a) => !a.isRead).slice(-5), quickLinks: this.getQuickLinks(developer.tier), }; } @@ -225,20 +255,22 @@ export class DeveloperPortalService { async getDocumentation(category?: string): Promise { const docs = Array.from(this.documentation.values()); if (category) { - return docs.filter(doc => doc.category === category && doc.isPublished); + return docs.filter((doc) => doc.category === category && doc.isPublished); } - return docs.filter(doc => doc.isPublished); + return docs.filter((doc) => doc.isPublished); } async getIntegrationGuides(platform?: string): Promise { const guides = Array.from(this.integrationGuides.values()); if (platform) { - return guides.filter(guide => guide.platform.toLowerCase() === platform.toLowerCase()); + return guides.filter((guide) => guide.platform.toLowerCase() === platform.toLowerCase()); } return guides; } - async addDocumentation(doc: Omit): Promise { + async addDocumentation( + doc: Omit + ): Promise { const documentation: Documentation = { ...doc, id: this.generateDocId(), @@ -261,7 +293,7 @@ export class DeveloperPortalService { } private findDeveloperByEmail(email: string): Developer | undefined { - return Array.from(this.developers.values()).find(dev => dev.email === email); + return Array.from(this.developers.values()).find((dev) => dev.email === email); } private generateDeveloperId(): string { @@ -285,14 +317,34 @@ export class DeveloperPortalService { return `guide_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } - private getDefaultRateLimit(tier: string): { requestsPerMinute: number; requestsPerHour: number; requestsPerDay: number; burstLimit: number } { + private getDefaultRateLimit(tier: string): { + requestsPerMinute: number; + requestsPerHour: number; + requestsPerDay: number; + burstLimit: number; + } { switch (tier) { case 'enterprise': - return { requestsPerMinute: 1000, requestsPerHour: 50000, requestsPerDay: 500000, burstLimit: 2000 }; + return { + requestsPerMinute: 1000, + requestsPerHour: 50000, + requestsPerDay: 500000, + burstLimit: 2000, + }; case 'pro': - return { requestsPerMinute: 100, requestsPerHour: 5000, requestsPerDay: 50000, burstLimit: 200 }; + return { + requestsPerMinute: 100, + requestsPerHour: 5000, + requestsPerDay: 50000, + burstLimit: 200, + }; default: - return { requestsPerMinute: 20, requestsPerHour: 1000, requestsPerDay: 10000, burstLimit: 50 }; + return { + requestsPerMinute: 20, + requestsPerHour: 1000, + requestsPerDay: 10000, + burstLimit: 50, + }; } } @@ -321,7 +373,13 @@ export class DeveloperPortalService { }; } - private async logActivity(developerId: string, action: string, resource: string, resourceId: string, details?: Record): Promise { + private async logActivity( + developerId: string, + action: string, + resource: string, + resourceId: string, + details?: Record + ): Promise { const logs = this.activityLogs.get(developerId) || []; logs.push({ id: `log_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, @@ -334,7 +392,13 @@ export class DeveloperPortalService { this.activityLogs.set(developerId, logs); } - private async createAlert(developerId: string, type: 'info' | 'warning' | 'error' | 'success', title: string, message: string, actionUrl?: string): Promise { + private async createAlert( + developerId: string, + type: 'info' | 'warning' | 'error' | 'success', + title: string, + message: string, + actionUrl?: string + ): Promise { const alerts = this.alerts.get(developerId) || []; alerts.push({ id: `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, @@ -348,20 +412,52 @@ export class DeveloperPortalService { this.alerts.set(developerId, alerts); } - private getQuickLinks(tier: string): Array<{ title: string; description: string; url: string; icon: string }> { + private getQuickLinks( + tier: string + ): { title: string; description: string; url: string; icon: string }[] { const links = [ - { title: 'API Documentation', description: 'View API reference documentation', url: '/docs/api', icon: 'book' }, - { title: 'Sandbox', description: 'Access your sandbox environment', url: '/sandbox', icon: 'code' }, - { title: 'API Keys', description: 'Manage your API keys', url: '/settings/api-keys', icon: 'key' }, - { title: 'Usage', description: 'View your API usage', url: '/analytics/usage', icon: 'chart' }, + { + title: 'API Documentation', + description: 'View API reference documentation', + url: '/docs/api', + icon: 'book', + }, + { + title: 'Sandbox', + description: 'Access your sandbox environment', + url: '/sandbox', + icon: 'code', + }, + { + title: 'API Keys', + description: 'Manage your API keys', + url: '/settings/api-keys', + icon: 'key', + }, + { + title: 'Usage', + description: 'View your API usage', + url: '/analytics/usage', + icon: 'chart', + }, ]; if (tier === 'pro' || tier === 'enterprise') { - links.push({ title: 'Webhooks', description: 'Configure webhooks', url: '/settings/webhooks', icon: 'webhook' }); + links.push({ + title: 'Webhooks', + description: 'Configure webhooks', + url: '/settings/webhooks', + icon: 'webhook', + }); } if (tier === 'enterprise') { - links.push({ title: 'Team', description: 'Manage team members', url: '/settings/team', icon: 'users' }); + links.push({ + title: 'Team', + description: 'Manage team members', + url: '/settings/team', + icon: 'users', + }); } return links; diff --git a/developer-portal/services/documentationService.ts b/developer-portal/services/documentationService.ts index 122555fd..60cb95f4 100644 --- a/developer-portal/services/documentationService.ts +++ b/developer-portal/services/documentationService.ts @@ -278,8 +278,8 @@ app.post('/webhooks', (req, res) => { }, ]; - this.sections.forEach(section => { - section.articles.forEach(article => { + this.sections.forEach((section) => { + section.articles.forEach((article) => { this.articles.set(article.slug, article); }); }); @@ -310,7 +310,7 @@ app.post('/webhooks', (req, res) => { } async getSection(sectionId: string): Promise { - return this.sections.find(s => s.id === sectionId) || null; + return this.sections.find((s) => s.id === sectionId) || null; } async getArticle(slug: string): Promise { @@ -321,12 +321,10 @@ app.post('/webhooks', (req, res) => { const lowerQuery = query.toLowerCase(); const results: DocumentationArticle[] = []; - this.articles.forEach(article => { + this.articles.forEach((article) => { const matchesTitle = article.title.toLowerCase().includes(lowerQuery); const matchesContent = article.content.toLowerCase().includes(lowerQuery); - const matchesTags = article.tags.some(tag => - tag.toLowerCase().includes(lowerQuery) - ); + const matchesTags = article.tags.some((tag) => tag.toLowerCase().includes(lowerQuery)); if (matchesTitle || matchesContent || matchesTags) { results.push(article); @@ -347,7 +345,7 @@ app.post('/webhooks', (req, res) => { if (!article) return []; return Array.from(this.articles.values()) - .filter(a => a.slug !== slug && a.category === article.category) + .filter((a) => a.slug !== slug && a.category === article.category) .slice(0, 3); } } diff --git a/developer-portal/services/integrationGuidesService.ts b/developer-portal/services/integrationGuidesService.ts index 52d2049c..c16cda9f 100644 --- a/developer-portal/services/integrationGuidesService.ts +++ b/developer-portal/services/integrationGuidesService.ts @@ -1,4 +1,4 @@ -import { IntegrationGuide, IntegrationStep } from '../types/portal'; +import { IntegrationGuide } from '../types/portal'; export class IntegrationGuidesService { private guides: IntegrationGuide[] = []; @@ -298,19 +298,19 @@ const subtrackr = new SubTrackr({ } async getGuide(guideId: string): Promise { - return this.guides.find(g => g.id === guideId) || null; + return this.guides.find((g) => g.id === guideId) || null; } async getGuidesByDifficulty( difficulty: IntegrationGuide['difficulty'] ): Promise { - return this.guides.filter(g => g.difficulty === difficulty); + return this.guides.filter((g) => g.difficulty === difficulty); } async searchGuides(query: string): Promise { const lowerQuery = query.toLowerCase(); return this.guides.filter( - guide => + (guide) => guide.title.toLowerCase().includes(lowerQuery) || guide.description.toLowerCase().includes(lowerQuery) ); diff --git a/developer-portal/services/portalService.ts b/developer-portal/services/portalService.ts index 7ccd4c0d..fef88a02 100644 --- a/developer-portal/services/portalService.ts +++ b/developer-portal/services/portalService.ts @@ -31,9 +31,7 @@ export class DeveloperPortalService { company: string, role: PortalUser['role'] = 'developer' ): Promise { - const existingUser = Array.from(this.users.values()).find( - u => u.email === email - ); + const existingUser = Array.from(this.users.values()).find((u) => u.email === email); if (existingUser) { throw new Error('User already exists'); @@ -90,9 +88,7 @@ export class DeveloperPortalService { this.activities.set(userId, activities.slice(0, 100)); } - private async getEnvironmentSummaries( - userId: string - ): Promise { + private async getEnvironmentSummaries(_userId: string): Promise { return [ { id: '1', diff --git a/developer-portal/src/components/DashboardCard.tsx b/developer-portal/src/components/DashboardCard.tsx index b94f23f6..a3704584 100644 --- a/developer-portal/src/components/DashboardCard.tsx +++ b/developer-portal/src/components/DashboardCard.tsx @@ -46,9 +46,7 @@ export const DashboardCard: React.FC = ({ {title} {value} - {trend && ( - {getTrendIcon()} - )} + {trend && {getTrendIcon()}} {subtitle && {subtitle}} diff --git a/developer-portal/src/components/PermissionSelector.tsx b/developer-portal/src/components/PermissionSelector.tsx index 90d83336..5c4bf482 100644 --- a/developer-portal/src/components/PermissionSelector.tsx +++ b/developer-portal/src/components/PermissionSelector.tsx @@ -58,7 +58,8 @@ export const PermissionSelector: React.FC = ({ {permission.icon} - + {permission.label} {permission.description} diff --git a/developer-portal/src/components/QuickActionCard.tsx b/developer-portal/src/components/QuickActionCard.tsx index 9089c703..4f72f41e 100644 --- a/developer-portal/src/components/QuickActionCard.tsx +++ b/developer-portal/src/components/QuickActionCard.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { Text, StyleSheet, TouchableOpacity } from 'react-native'; interface QuickActionCardProps { icon: string; diff --git a/developer-portal/src/screens/ApiKeyManagementScreen.tsx b/developer-portal/src/screens/ApiKeyManagementScreen.tsx index b7870c25..d4298258 100644 --- a/developer-portal/src/screens/ApiKeyManagementScreen.tsx +++ b/developer-portal/src/screens/ApiKeyManagementScreen.tsx @@ -21,7 +21,6 @@ const ApiKeyManagementScreen: React.FC = () => { const { developer, apiKeys, - isLoading, fetchApiKeys, createApiKey, revokeApiKey, @@ -152,13 +151,9 @@ const ApiKeyManagementScreen: React.FC = () => { API Key Management - - Manage your API keys and configure permissions - + Manage your API keys and configure permissions - setShowCreateModal(true)}> + setShowCreateModal(true)}> + New Key @@ -245,12 +240,9 @@ const ApiKeyManagementScreen: React.FC = () => { 🔑 No API Keys Yet - Create your first API key to start making authenticated requests to the SubTrackr - API + Create your first API key to start making authenticated requests to the SubTrackr API - setShowCreateModal(true)}> + setShowCreateModal(true)}> Create API Key @@ -311,11 +303,7 @@ const ApiKeyManagementScreen: React.FC = () => { Create API Key - + Create @@ -405,9 +393,7 @@ const ApiKeyManagementScreen: React.FC = () => { }}> Copy to Clipboard - setCreatedKey(null)}> + setCreatedKey(null)}> I've Saved My Key diff --git a/developer-portal/src/screens/ApiTesterScreen.tsx b/developer-portal/src/screens/ApiTesterScreen.tsx index c69ee6c4..aeb4eb99 100644 --- a/developer-portal/src/screens/ApiTesterScreen.tsx +++ b/developer-portal/src/screens/ApiTesterScreen.tsx @@ -23,7 +23,7 @@ const EXAMPLE_ENDPOINTS = [ ]; const ApiTesterScreen: React.FC = () => { - const { developer, apiKeys } = useDeveloperPortalStore(); + const { apiKeys } = useDeveloperPortalStore(); const [selectedMethod, setSelectedMethod] = useState('GET'); const [endpoint, setEndpoint] = useState('/api/v1/subscriptions'); const [selectedApiKey, setSelectedApiKey] = useState(''); @@ -90,7 +90,7 @@ const ApiTesterScreen: React.FC = () => { } }; - const loadExample = (example: typeof EXAMPLE_ENDPOINTS[0]) => { + const loadExample = (example: (typeof EXAMPLE_ENDPOINTS)[0]) => { setSelectedMethod(example.method); setEndpoint(example.path); if (example.method === 'POST' || example.method === 'PUT') { @@ -133,10 +133,7 @@ const ApiTesterScreen: React.FC = () => { {activeKeys.map((key) => ( setSelectedApiKey(key.id)}> { Request - + {HTTP_METHODS.map((method) => ( { Response - {responseTime && ( - {responseTime}ms - )} + {responseTime && {responseTime}ms} + style={[styles.statusBadge, { backgroundColor: getStatusColor(response.status) }]}> {response.status} {response.statusText} diff --git a/developer-portal/src/screens/DeveloperPortalScreen.tsx b/developer-portal/src/screens/DeveloperPortalScreen.tsx index f8975505..8e4d54d5 100644 --- a/developer-portal/src/screens/DeveloperPortalScreen.tsx +++ b/developer-portal/src/screens/DeveloperPortalScreen.tsx @@ -26,7 +26,6 @@ const DeveloperPortalScreen: React.FC<{ navigation: any }> = ({ navigation }) => onboardingSteps, isLoading, error, - fetchDeveloper, fetchApiKeys, fetchUsageStats, fetchRecentUsage, @@ -70,34 +69,29 @@ const DeveloperPortalScreen: React.FC<{ navigation: any }> = ({ navigation }) => const handleQuickCreateApiKey = async () => { if (!developer) return; - Alert.prompt( - 'Create API Key', - 'Enter a name for your new API key', - async (name) => { - if (name && name.trim()) { - try { - const newKey = await createApiKey( - developer.id, - name.trim(), - [ApiKeyPermission.READ, ApiKeyPermission.WRITE] - ); - Alert.alert( - 'API Key Created', - `Your new API key has been created:\n\n${newKey.key}\n\nCopy it now - it won't be shown again.`, - [ - { - text: 'View All Keys', - onPress: () => navigation.navigate('ApiKeyManagement'), - }, - { text: 'OK' }, - ] - ); - } catch (err) { - Alert.alert('Error', 'Failed to create API key'); - } + Alert.prompt('Create API Key', 'Enter a name for your new API key', async (name) => { + if (name && name.trim()) { + try { + const newKey = await createApiKey(developer.id, name.trim(), [ + ApiKeyPermission.READ, + ApiKeyPermission.WRITE, + ]); + Alert.alert( + 'API Key Created', + `Your new API key has been created:\n\n${newKey.key}\n\nCopy it now - it won't be shown again.`, + [ + { + text: 'View All Keys', + onPress: () => navigation.navigate('ApiKeyManagement'), + }, + { text: 'OK' }, + ] + ); + } catch (err) { + Alert.alert('Error', 'Failed to create API key'); } } - ); + }); }; if (!developer) { @@ -105,9 +99,7 @@ const DeveloperPortalScreen: React.FC<{ navigation: any }> = ({ navigation }) => Welcome to Developer Portal - - Please register to access the developer portal - + Please register to access the developer portal navigation.navigate('DeveloperRegistration')}> @@ -303,9 +295,7 @@ const DeveloperPortalScreen: React.FC<{ navigation: any }> = ({ navigation }) => 💬 Developer Support - - Get help from our developer community - + Get help from our developer community diff --git a/developer-portal/src/screens/SdkDownloadScreen.tsx b/developer-portal/src/screens/SdkDownloadScreen.tsx index df9bd16c..b544251f 100644 --- a/developer-portal/src/screens/SdkDownloadScreen.tsx +++ b/developer-portal/src/screens/SdkDownloadScreen.tsx @@ -113,7 +113,7 @@ subscription = client.subscriptions.create( ]; const SdkDownloadScreen: React.FC = () => { - const handleCopyInstall = (installCommand: string) => { + const handleCopyInstall = (_installCommand: string) => { // Copy to clipboard logic Alert.alert('Copied', 'Install command copied to clipboard'); }; diff --git a/developer-portal/src/screens/UsageAnalyticsScreen.tsx b/developer-portal/src/screens/UsageAnalyticsScreen.tsx index 5f373387..7ec8f107 100644 --- a/developer-portal/src/screens/UsageAnalyticsScreen.tsx +++ b/developer-portal/src/screens/UsageAnalyticsScreen.tsx @@ -183,8 +183,8 @@ const UsageAnalyticsScreen: React.FC = () => { request.statusCode >= 200 && request.statusCode < 300 ? '#4CAF50' : request.statusCode >= 400 && request.statusCode < 500 - ? '#FF9800' - : '#F44336', + ? '#FF9800' + : '#F44336', }, ]}> {request.statusCode} @@ -207,9 +207,7 @@ const UsageAnalyticsScreen: React.FC = () => { Current Usage - - {usageStats?.totalCalls || 0} / 10,000 - + {usageStats?.totalCalls || 0} / 10,000 { autoCapitalize="none" keyboardType="url" /> - - Enter the URL where you want to receive webhook events - + Enter the URL where you want to receive webhook events {/* Event Types */} @@ -147,9 +145,7 @@ const WebhookTesterScreen: React.FC = () => { styles.checkbox, selectedEvents.includes(event.value) && styles.checkboxSelected, ]}> - {selectedEvents.includes(event.value) && ( - - )} + {selectedEvents.includes(event.value) && } ))} @@ -171,9 +167,7 @@ const WebhookTesterScreen: React.FC = () => { Generate - - Use this secret to verify webhook signatures - + Use this secret to verify webhook signatures {/* Signature Option */} @@ -235,9 +229,7 @@ const WebhookTesterScreen: React.FC = () => { {new Date(testResults.timestamp).toLocaleString()} )} - {testResults.error && ( - {testResults.error} - )} + {testResults.error && {testResults.error}} )} diff --git a/developer-portal/types/developer.ts b/developer-portal/types/developer.ts index 538128c5..2e2b9339 100644 --- a/developer-portal/types/developer.ts +++ b/developer-portal/types/developer.ts @@ -16,7 +16,12 @@ export interface Developer { } export interface OnboardingStatus { - step: 'registration' | 'email_verification' | 'profile_completion' | 'sandbox_setup' | 'completed'; + step: + | 'registration' + | 'email_verification' + | 'profile_completion' + | 'sandbox_setup' + | 'completed'; completedSteps: string[]; startedAt: Date; completedAt?: Date; diff --git a/developer-portal/types/portal.ts b/developer-portal/types/portal.ts index 3a5396f1..32687b89 100644 --- a/developer-portal/types/portal.ts +++ b/developer-portal/types/portal.ts @@ -26,7 +26,12 @@ export interface EnvironmentSummary { export interface ActivityEntry { id: string; - type: 'api_key_created' | 'environment_created' | 'request_made' | 'error_occurred' | 'webhook_triggered'; + type: + | 'api_key_created' + | 'environment_created' + | 'request_made' + | 'error_occurred' + | 'webhook_triggered'; description: string; timestamp: Date; metadata?: Record; diff --git a/developer-portal/utils/developerPortalUtils.ts b/developer-portal/utils/developerPortalUtils.ts index 563d256e..8d186140 100644 --- a/developer-portal/utils/developerPortalUtils.ts +++ b/developer-portal/utils/developerPortalUtils.ts @@ -1,4 +1,4 @@ -import { Developer, ApiKey, ApiPermission, UsageMetrics, RateLimit } from '../types/developer'; +import { Developer, ApiKey, ApiPermission, UsageMetrics } from '../types/developer'; export class DeveloperPortalUtils { static validateEmail(email: string): boolean { @@ -6,7 +6,11 @@ export class DeveloperPortalUtils { return emailRegex.test(email); } - static validateApiKey(key: string): { valid: boolean; type?: 'test' | 'production'; error?: string } { + static validateApiKey(key: string): { + valid: boolean; + type?: 'test' | 'production'; + error?: string; + } { if (!key) { return { valid: false, error: 'API key is required' }; } @@ -31,7 +35,10 @@ export class DeveloperPortalUtils { return apiKey.permissions.includes(permission); } - static checkRateLimit(apiKey: ApiKey, currentUsage: { requestsPerMinute: number; requestsPerHour: number; requestsPerDay: number }): { allowed: boolean; retryAfter?: number } { + static checkRateLimit( + apiKey: ApiKey, + currentUsage: { requestsPerMinute: number; requestsPerHour: number; requestsPerDay: number } + ): { allowed: boolean; retryAfter?: number } { if (currentUsage.requestsPerMinute >= apiKey.rateLimit.requestsPerMinute) { return { allowed: false, retryAfter: 60 }; } @@ -62,13 +69,15 @@ export class DeveloperPortalUtils { avgResponseTime: string; errorRate: string; } { - const successRate = metrics.totalRequests > 0 - ? ((metrics.successfulRequests / metrics.totalRequests) * 100).toFixed(2) - : '0.00'; + const successRate = + metrics.totalRequests > 0 + ? ((metrics.successfulRequests / metrics.totalRequests) * 100).toFixed(2) + : '0.00'; - const errorRate = metrics.totalRequests > 0 - ? ((metrics.failedRequests / metrics.totalRequests) * 100).toFixed(2) - : '0.00'; + const errorRate = + metrics.totalRequests > 0 + ? ((metrics.failedRequests / metrics.totalRequests) * 100).toFixed(2) + : '0.00'; return { totalRequests: metrics.totalRequests.toLocaleString(), @@ -78,7 +87,9 @@ export class DeveloperPortalUtils { }; } - static generateOnboardingChecklist(developer: Developer): Array<{ step: string; completed: boolean; description: string }> { + static generateOnboardingChecklist( + developer: Developer + ): { step: string; completed: boolean; description: string }[] { return [ { step: 'register', @@ -142,7 +153,14 @@ export class DeveloperPortalUtils { maxApiKeys: 50, maxSandboxEnvironments: 10, maxRequestsPerMonth: 1000000, - features: ['priority_support', 'custom_sla', 'dedicated_account_manager', 'advanced_analytics', 'webhooks', 'team_management'], + features: [ + 'priority_support', + 'custom_sla', + 'dedicated_account_manager', + 'advanced_analytics', + 'webhooks', + 'team_management', + ], }; case 'pro': return { @@ -180,9 +198,9 @@ export class DeveloperPortalUtils { } static generateWebhookSecret(): string { - return 'whsec_' + Array.from({ length: 32 }, () => - Math.random().toString(36).charAt(2) - ).join(''); + return ( + 'whsec_' + Array.from({ length: 32 }, () => Math.random().toString(36).charAt(2)).join('') + ); } static formatCurrency(amount: number, currency: string = 'USD'): string { diff --git a/sandbox/__tests__/developerPortal.test.ts b/sandbox/__tests__/developerPortal.test.ts index 5aca0a27..0ce5972d 100644 --- a/sandbox/__tests__/developerPortal.test.ts +++ b/sandbox/__tests__/developerPortal.test.ts @@ -10,11 +10,7 @@ describe('DeveloperPortalService', () => { describe('createUser', () => { it('should create a new user', async () => { - const user = await service.createUser( - 'test@example.com', - 'Test User', - 'Test Company' - ); + const user = await service.createUser('test@example.com', 'Test User', 'Test Company'); expect(user).toBeDefined(); expect(user.email).toBe('test@example.com'); @@ -26,9 +22,9 @@ describe('DeveloperPortalService', () => { it('should not allow duplicate email', async () => { await service.createUser('test@example.com', 'User 1', 'Company 1'); - await expect( - service.createUser('test@example.com', 'User 2', 'Company 2') - ).rejects.toThrow('User already exists'); + await expect(service.createUser('test@example.com', 'User 2', 'Company 2')).rejects.toThrow( + 'User already exists' + ); }); it('should create user with custom role', async () => { @@ -50,11 +46,7 @@ describe('DeveloperPortalService', () => { }); it('should return existing user', async () => { - const created = await service.createUser( - 'test@example.com', - 'Test User', - 'Test Company' - ); + const created = await service.createUser('test@example.com', 'Test User', 'Test Company'); const retrieved = await service.getUser(created.id); expect(retrieved).toBeDefined(); @@ -64,11 +56,7 @@ describe('DeveloperPortalService', () => { describe('updateUser', () => { it('should update user details', async () => { - const user = await service.createUser( - 'test@example.com', - 'Test User', - 'Test Company' - ); + const user = await service.createUser('test@example.com', 'Test User', 'Test Company'); const updated = await service.updateUser(user.id, { name: 'Updated Name', @@ -91,11 +79,7 @@ describe('DeveloperPortalService', () => { describe('getDashboard', () => { it('should return dashboard data', async () => { - const user = await service.createUser( - 'test@example.com', - 'Test User', - 'Test Company' - ); + const user = await service.createUser('test@example.com', 'Test User', 'Test Company'); const dashboard = await service.getDashboard(user.id); @@ -111,25 +95,15 @@ describe('DeveloperPortalService', () => { }); it('should throw for non-existent user', async () => { - await expect(service.getDashboard('non-existent')).rejects.toThrow( - 'User not found' - ); + await expect(service.getDashboard('non-existent')).rejects.toThrow('User not found'); }); }); describe('logActivity', () => { it('should log user activity', async () => { - const user = await service.createUser( - 'test@example.com', - 'Test User', - 'Test Company' - ); + const user = await service.createUser('test@example.com', 'Test User', 'Test Company'); - await service.logActivity( - user.id, - 'api_key_created', - 'New API key created' - ); + await service.logActivity(user.id, 'api_key_created', 'New API key created'); const dashboard = await service.getDashboard(user.id); expect(dashboard.recentActivity.length).toBeGreaterThan(0); diff --git a/sandbox/__tests__/sandbox.test.ts b/sandbox/__tests__/sandbox.test.ts index edb3315b..0c0a2094 100644 --- a/sandbox/__tests__/sandbox.test.ts +++ b/sandbox/__tests__/sandbox.test.ts @@ -1,8 +1,6 @@ import { SandboxIsolationService } from '../services/sandboxIsolationService'; import { ApiKeyService } from '../services/apiKeyService'; import { UsageTrackingService } from '../services/usageTrackingService'; -import { SandboxService } from '../services/sandboxService'; -import { SandboxMiddleware } from '../middleware/sandboxMiddleware'; import { SandboxUtils } from '../utils/sandboxUtils'; describe('SandboxIsolationService', () => { @@ -135,22 +133,12 @@ describe('SandboxIsolationService', () => { describe('updateOnboardingStep', () => { it('should update onboarding step', async () => { - const developer = await service.registerDeveloper( - 'test@example.com', - 'Test Dev', - 'Test Co' - ); + const developer = await service.registerDeveloper('test@example.com', 'Test Dev', 'Test Co'); - const updated = await service.updateOnboardingStep( - developer.id, - 'create-sandbox', - true - ); + const updated = await service.updateOnboardingStep(developer.id, 'create-sandbox', true); expect(updated).toBeDefined(); - const step = updated?.onboardingStatus.steps.find( - (s) => s.id === 'create-sandbox' - ); + const step = updated?.onboardingStatus.steps.find((s) => s.id === 'create-sandbox'); expect(step?.completed).toBe(true); }); }); @@ -179,9 +167,9 @@ describe('ApiKeyService', () => { await service.generateApiKey('env-1', `Key ${i}`, ['read']); } - await expect( - service.generateApiKey('env-1', 'Extra Key', ['read']) - ).rejects.toThrow('Maximum API keys limit reached'); + await expect(service.generateApiKey('env-1', 'Extra Key', ['read'])).rejects.toThrow( + 'Maximum API keys limit reached' + ); }); }); diff --git a/sandbox/api/sandboxApi.ts b/sandbox/api/sandboxApi.ts index d40486c0..5dc2ca66 100644 --- a/sandbox/api/sandboxApi.ts +++ b/sandbox/api/sandboxApi.ts @@ -18,11 +18,7 @@ export class SandboxApi { config?: Partial ): Promise { try { - const environment = await this.sandboxService.createEnvironment( - developerId, - name, - config - ); + const environment = await this.sandboxService.createEnvironment(developerId, name, config); return { success: true, @@ -76,10 +72,7 @@ export class SandboxApi { updates: Partial ): Promise { try { - const environment = await this.sandboxService.updateConfig( - environmentId, - updates - ); + const environment = await this.sandboxService.updateConfig(environmentId, updates); if (!environment) { return { success: false, error: 'Environment not found' }; diff --git a/sandbox/config/sandboxConfig.ts b/sandbox/config/sandboxConfig.ts index 25b8ceae..d9c94816 100644 --- a/sandbox/config/sandboxConfig.ts +++ b/sandbox/config/sandboxConfig.ts @@ -118,9 +118,12 @@ export const SANDBOX_CONSTANTS = { DEFAULT_ENVIRONMENT_TTL_DAYS: 90, }; -export function getSandboxConfig( - tier: 'free' | 'pro' | 'enterprise' = 'free' -): { resourceLimits: SandboxResourceLimits; rateLimits: RateLimit; features: SandboxFeatures; dataRetentionDays: number } { +export function getSandboxConfig(tier: 'free' | 'pro' | 'enterprise' = 'free'): { + resourceLimits: SandboxResourceLimits; + rateLimits: RateLimit; + features: SandboxFeatures; + dataRetentionDays: number; +} { return SANDBOX_TIERS[tier] || SANDBOX_TIERS.free; } diff --git a/sandbox/middleware/sandboxMiddleware.ts b/sandbox/middleware/sandboxMiddleware.ts index 330eb6b5..b52929ce 100644 --- a/sandbox/middleware/sandboxMiddleware.ts +++ b/sandbox/middleware/sandboxMiddleware.ts @@ -1,4 +1,4 @@ -import { SandboxIsolationContext, SandboxResourceLimits } from '../types/sandbox'; +import { SandboxResourceLimits } from '../types/sandbox'; import { sandboxService } from '../services/sandboxService'; export interface SandboxRequest { @@ -24,18 +24,14 @@ export interface SandboxResponse { } export class SandboxMiddleware { - private rateLimitStore: Map = - new Map(); + private rateLimitStore: Map = new Map(); async processRequest(request: SandboxRequest): Promise { const startTime = Date.now(); const requestId = this.generateRequestId(); try { - const isValid = await sandboxService.validateAccess( - request.environmentId, - request.apiKey - ); + const isValid = await sandboxService.validateAccess(request.environmentId, request.apiKey); if (!isValid) { return this.createErrorResponse( request, @@ -46,9 +42,7 @@ export class SandboxMiddleware { ); } - const context = await sandboxService.getIsolationContext( - request.environmentId - ); + const context = await sandboxService.getIsolationContext(request.environmentId); if (!context) { return this.createErrorResponse( request, @@ -74,20 +68,10 @@ export class SandboxMiddleware { context.resourceQuota ); if (!rateLimitResult.allowed) { - return this.createErrorResponse( - request, - requestId, - startTime, - 'Rate limit exceeded', - 429 - ); + return this.createErrorResponse(request, requestId, startTime, 'Rate limit exceeded', 429); } - await sandboxService.recordRequest( - request.environmentId, - Date.now() - startTime, - false - ); + await sandboxService.recordRequest(request.environmentId, Date.now() - startTime, false); return { success: true, @@ -101,18 +85,8 @@ export class SandboxMiddleware { }, }; } catch (error) { - await sandboxService.recordRequest( - request.environmentId, - Date.now() - startTime, - true - ); - return this.createErrorResponse( - request, - requestId, - startTime, - 'Internal sandbox error', - 500 - ); + await sandboxService.recordRequest(request.environmentId, Date.now() - startTime, true); + return this.createErrorResponse(request, requestId, startTime, 'Internal sandbox error', 500); } } @@ -144,9 +118,7 @@ export class SandboxMiddleware { }; } - async enforceResourceLimits( - envId: string - ): Promise<{ withinLimits: boolean; usage: unknown }> { + async enforceResourceLimits(envId: string): Promise<{ withinLimits: boolean; usage: unknown }> { const context = await sandboxService.getIsolationContext(envId); if (!context) throw new Error('Environment not found'); diff --git a/sandbox/services/apiKeyService.ts b/sandbox/services/apiKeyService.ts index b04f67ad..83b94a0c 100644 --- a/sandbox/services/apiKeyService.ts +++ b/sandbox/services/apiKeyService.ts @@ -75,8 +75,10 @@ export class ApiKeyService { return false; } - return validation.apiKey.permissions.includes(permission) || - validation.apiKey.permissions.includes('admin'); + return ( + validation.apiKey.permissions.includes(permission) || + validation.apiKey.permissions.includes('admin') + ); } async revokeApiKey(keyId: string): Promise { diff --git a/sandbox/services/sandboxIsolationService.ts b/sandbox/services/sandboxIsolationService.ts index 5b54fe1f..076710cd 100644 --- a/sandbox/services/sandboxIsolationService.ts +++ b/sandbox/services/sandboxIsolationService.ts @@ -2,9 +2,7 @@ import { SandboxEnvironment, SandboxConfig, Developer, - SandboxFeatures, RateLimit, - ApiKey, SandboxTestData, } from '../types/sandbox'; import { createSandboxConfig, SANDBOX_CONSTANTS } from '../config/sandboxConfig'; @@ -65,9 +63,7 @@ export class SandboxIsolationService { } async getEnvironmentsByDeveloper(developerId: string): Promise { - return Array.from(this.environments.values()).filter( - (env) => env.developerId === developerId - ); + return Array.from(this.environments.values()).filter((env) => env.developerId === developerId); } async updateEnvironment( @@ -164,11 +160,7 @@ export class SandboxIsolationService { } } - private validateRateLimits( - rateLimits: RateLimit, - errors: string[], - _warnings: string[] - ): void { + private validateRateLimits(rateLimits: RateLimit, errors: string[], _warnings: string[]): void { if (rateLimits.requestsPerMinute <= 0) { errors.push('Invalid requestsPerMinute rate limit'); } @@ -180,14 +172,8 @@ export class SandboxIsolationService { } } - async registerDeveloper( - email: string, - name: string, - company: string - ): Promise { - const existingDeveloper = Array.from(this.developers.values()).find( - (d) => d.email === email - ); + async registerDeveloper(email: string, name: string, company: string): Promise { + const existingDeveloper = Array.from(this.developers.values()).find((d) => d.email === email); if (existingDeveloper) { throw new Error('Developer already registered'); @@ -268,8 +254,9 @@ export class SandboxIsolationService { developer.onboardingStatus.step = developer.onboardingStatus.steps.filter( (s) => s.completed ).length; - developer.onboardingStatus.completed = - developer.onboardingStatus.steps.every((s) => s.completed); + developer.onboardingStatus.completed = developer.onboardingStatus.steps.every( + (s) => s.completed + ); this.developers.set(developerId, developer); return developer; @@ -300,7 +287,16 @@ export class SandboxIsolationService { private generateTestSubscriptions(): SandboxTestData['subscriptions'] { const categories = ['streaming', 'software', 'gaming', 'productivity', 'fitness']; - const names = ['Netflix', 'Spotify', 'Adobe CC', 'Slack', 'Gym Membership', 'GitHub Pro', 'Figma', 'Notion']; + const names = [ + 'Netflix', + 'Spotify', + 'Adobe CC', + 'Slack', + 'Gym Membership', + 'GitHub Pro', + 'Figma', + 'Notion', + ]; return names.map((name, index) => ({ id: `sub_test_${index + 1}`, @@ -315,9 +311,11 @@ export class SandboxIsolationService { })); } - private generateTestPayments(subscriptions: SandboxTestData['subscriptions']): SandboxTestData['payments'] { + private generateTestPayments( + subscriptions: SandboxTestData['subscriptions'] + ): SandboxTestData['payments'] { const payments: SandboxTestData['payments'] = []; - const methods: Array<'card' | 'crypto' | 'bank'> = ['card', 'crypto', 'bank']; + const methods: ('card' | 'crypto' | 'bank')[] = ['card', 'crypto', 'bank']; subscriptions.forEach((sub) => { for (let i = 0; i < 3; i++) { diff --git a/sandbox/services/sandboxService.ts b/sandbox/services/sandboxService.ts index 9ff0936d..c9841367 100644 --- a/sandbox/services/sandboxService.ts +++ b/sandbox/services/sandboxService.ts @@ -23,7 +23,6 @@ export class SandboxService { config?: Partial ): Promise { const envId = this.generateEnvironmentId(); - const defaultResourceLimits = this.getDefaultResourceLimits(); const environment: SandboxEnvironment = { id: envId, @@ -91,9 +90,7 @@ export class SandboxService { } async getEnvironmentsByDeveloper(developerId: string): Promise { - return Array.from(this.environments.values()).filter( - (env) => env.developerId === developerId - ); + return Array.from(this.environments.values()).filter((env) => env.developerId === developerId); } async updateEnvironment( @@ -130,10 +127,7 @@ export class SandboxService { return this.configs.get(envId) || null; } - async updateConfig( - envId: string, - config: Partial - ): Promise { + async updateConfig(envId: string, config: Partial): Promise { const existingConfig = this.configs.get(envId); if (!existingConfig) return null; @@ -160,19 +154,14 @@ export class SandboxService { return this.metrics.get(envId) || null; } - async recordRequest( - envId: string, - responseTime: number, - isError: boolean - ): Promise { + async recordRequest(envId: string, responseTime: number, isError: boolean): Promise { const metrics = this.metrics.get(envId); if (!metrics) return; metrics.requestCount++; if (isError) metrics.errorCount++; metrics.avgResponseTime = - (metrics.avgResponseTime * (metrics.requestCount - 1) + responseTime) / - metrics.requestCount; + (metrics.avgResponseTime * (metrics.requestCount - 1) + responseTime) / metrics.requestCount; metrics.lastActivity = new Date(); this.metrics.set(envId, metrics); @@ -184,14 +173,16 @@ export class SandboxService { if (!env || !metrics) return null; const isWithinLimits = this.checkResourceLimits( - env.config.features ? { - maxRequestsPerMinute: env.config.rateLimits.requestsPerMinute, - maxRequestsPerDay: env.config.rateLimits.requestsPerDay, - maxStorageMB: 100, - maxConcurrentConnections: env.config.rateLimits.maxConcurrentRequests, - maxSubscriptions: 50, - maxWebhooks: 5, - } : this.getDefaultResourceLimits(), + env.config.features + ? { + maxRequestsPerMinute: env.config.rateLimits.requestsPerMinute, + maxRequestsPerDay: env.config.rateLimits.requestsPerDay, + maxStorageMB: 100, + maxConcurrentConnections: env.config.rateLimits.maxConcurrentRequests, + maxSubscriptions: 50, + maxWebhooks: 5, + } + : this.getDefaultResourceLimits(), metrics ); @@ -199,14 +190,16 @@ export class SandboxService { environmentId: envId, developerId: env.developerId, dataNamespace: `sandbox_${envId}`, - resourceQuota: env.config.features ? { - maxRequestsPerMinute: env.config.rateLimits.requestsPerMinute, - maxRequestsPerDay: env.config.rateLimits.requestsPerDay, - maxStorageMB: 100, - maxConcurrentConnections: env.config.rateLimits.maxConcurrentRequests, - maxSubscriptions: 50, - maxWebhooks: 5, - } : this.getDefaultResourceLimits(), + resourceQuota: env.config.features + ? { + maxRequestsPerMinute: env.config.rateLimits.requestsPerMinute, + maxRequestsPerDay: env.config.rateLimits.requestsPerDay, + maxStorageMB: 100, + maxConcurrentConnections: env.config.rateLimits.maxConcurrentRequests, + maxSubscriptions: 50, + maxWebhooks: 5, + } + : this.getDefaultResourceLimits(), currentUsage: metrics, isWithinLimits, }; @@ -266,22 +259,16 @@ export class SandboxService { category: categories[index % categories.length], price: Math.floor(Math.random() * 50) + 5, currency: 'USD', - billingCycle: (['monthly', 'yearly', 'weekly'] as const)[ - Math.floor(Math.random() * 3) - ], + billingCycle: (['monthly', 'yearly', 'weekly'] as const)[Math.floor(Math.random() * 3)], status: 'active' as const, - startDate: new Date( - Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000 - ), - nextBillingDate: new Date( - Date.now() + Math.random() * 30 * 24 * 60 * 60 * 1000 - ), + startDate: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000), + nextBillingDate: new Date(Date.now() + Math.random() * 30 * 24 * 60 * 60 * 1000), })); } private generateTestPayments(subscriptions: TestSubscription[]): TestPayment[] { const payments: TestPayment[] = []; - const methods: Array<'card' | 'crypto' | 'bank'> = ['card', 'crypto', 'bank']; + const methods: ('card' | 'crypto' | 'bank')[] = ['card', 'crypto', 'bank']; subscriptions.forEach((sub) => { for (let i = 0; i < 3; i++) { @@ -290,13 +277,9 @@ export class SandboxService { subscriptionId: sub.id, amount: sub.price, currency: sub.currency, - status: (['pending', 'completed', 'failed'] as const)[ - Math.floor(Math.random() * 3) - ], + status: (['pending', 'completed', 'failed'] as const)[Math.floor(Math.random() * 3)], method: methods[Math.floor(Math.random() * methods.length)], - timestamp: new Date( - Date.now() - Math.random() * 90 * 24 * 60 * 60 * 1000 - ), + timestamp: new Date(Date.now() - Math.random() * 90 * 24 * 60 * 60 * 1000), }); } }); @@ -337,10 +320,7 @@ export class SandboxService { ]; } - private checkResourceLimits( - limits: SandboxResourceLimits, - metrics: SandboxMetrics - ): boolean { + private checkResourceLimits(limits: SandboxResourceLimits, metrics: SandboxMetrics): boolean { return ( metrics.requestCount < limits.maxRequestsPerDay && metrics.storageUsedMB < limits.maxStorageMB && diff --git a/sandbox/services/usageTrackingService.ts b/sandbox/services/usageTrackingService.ts index 1bf1f9c0..dc9cbaa0 100644 --- a/sandbox/services/usageTrackingService.ts +++ b/sandbox/services/usageTrackingService.ts @@ -1,8 +1,4 @@ -import { - UsageMetrics, - HourlyUsage, - DailyUsage, -} from '../types/sandbox'; +import { UsageMetrics } from '../types/sandbox'; export class UsageTrackingService { private usageData: Map = new Map(); @@ -60,26 +56,18 @@ export class UsageTrackingService { statusCode?: number; } ): Promise { - let filtered = this.requestLog.filter( - (entry) => entry.environmentId === environmentId - ); + let filtered = this.requestLog.filter((entry) => entry.environmentId === environmentId); if (options?.startDate) { - filtered = filtered.filter( - (entry) => entry.timestamp >= options.startDate! - ); + filtered = filtered.filter((entry) => entry.timestamp >= options.startDate!); } if (options?.endDate) { - filtered = filtered.filter( - (entry) => entry.timestamp <= options.endDate! - ); + filtered = filtered.filter((entry) => entry.timestamp <= options.endDate!); } if (options?.statusCode) { - filtered = filtered.filter( - (entry) => entry.statusCode === options.statusCode - ); + filtered = filtered.filter((entry) => entry.statusCode === options.statusCode); } filtered.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); @@ -111,9 +99,7 @@ export class UsageTrackingService { successfulRequests: metrics.successfulRequests, failedRequests: metrics.failedRequests, successRate: - metrics.totalRequests > 0 - ? (metrics.successfulRequests / metrics.totalRequests) * 100 - : 0, + metrics.totalRequests > 0 ? (metrics.successfulRequests / metrics.totalRequests) * 100 : 0, averageResponseTime: metrics.averageResponseTime, requestsLast24Hours: last24Hours.length, requestsLast7Days: last7Days.length, @@ -124,9 +110,7 @@ export class UsageTrackingService { async resetUsage(environmentId: string): Promise { this.usageData.delete(environmentId); - this.requestLog = this.requestLog.filter( - (entry) => entry.environmentId !== environmentId - ); + this.requestLog = this.requestLog.filter((entry) => entry.environmentId !== environmentId); return true; } @@ -159,9 +143,7 @@ export class UsageTrackingService { } private calculateAverageResponseTime(environmentId: string): number { - const entries = this.requestLog.filter( - (entry) => entry.environmentId === environmentId - ); + const entries = this.requestLog.filter((entry) => entry.environmentId === environmentId); if (entries.length === 0) return 0; @@ -169,11 +151,7 @@ export class UsageTrackingService { return Math.round(total / entries.length); } - private updateHourlyUsage( - metrics: UsageMetrics, - statusCode: number, - responseTime: number - ): void { + private updateHourlyUsage(metrics: UsageMetrics, statusCode: number, responseTime: number): void { const currentHour = new Date().getHours(); const hourlyData = metrics.last24Hours[currentHour]; @@ -189,11 +167,7 @@ export class UsageTrackingService { } } - private updateDailyUsage( - metrics: UsageMetrics, - statusCode: number, - responseTime: number - ): void { + private updateDailyUsage(metrics: UsageMetrics, statusCode: number, responseTime: number): void { const today = new Date().toISOString().split('T')[0]; const dailyData = metrics.last7Days.find((d) => d.date === today); @@ -203,16 +177,12 @@ export class UsageTrackingService { dailyData.errors++; } dailyData.avgResponseTime = Math.round( - (dailyData.avgResponseTime * (dailyData.requests - 1) + responseTime) / - dailyData.requests + (dailyData.avgResponseTime * (dailyData.requests - 1) + responseTime) / dailyData.requests ); } } - private getTopEndpoints( - environmentId: string, - limit: number - ): EndpointUsage[] { + private getTopEndpoints(environmentId: string, limit: number): EndpointUsage[] { const endpointMap = new Map(); this.requestLog @@ -232,10 +202,7 @@ export class UsageTrackingService { const errorMap = new Map(); this.requestLog - .filter( - (entry) => - entry.environmentId === environmentId && entry.statusCode >= 400 - ) + .filter((entry) => entry.environmentId === environmentId && entry.statusCode >= 400) .forEach((entry) => { errorMap.set(entry.statusCode, (errorMap.get(entry.statusCode) || 0) + 1); }); diff --git a/sandbox/utils/sandboxUtils.ts b/sandbox/utils/sandboxUtils.ts index 3f0692ba..0094ab68 100644 --- a/sandbox/utils/sandboxUtils.ts +++ b/sandbox/utils/sandboxUtils.ts @@ -25,7 +25,10 @@ export class SandboxUtils { return { valid: true }; } - static calculateResourceUsage(testData: SandboxTestData): { storageMB: number; itemCount: number } { + static calculateResourceUsage(testData: SandboxTestData): { + storageMB: number; + itemCount: number; + } { const jsonString = JSON.stringify(testData); const bytes = new TextEncoder().encode(jsonString).length; const storageMB = bytes / (1024 * 1024); @@ -41,24 +44,37 @@ export class SandboxUtils { static checkResourceLimits( limits: SandboxResourceLimits, - currentUsage: { requestsPerMinute: number; requestsPerDay: number; storageMB: number; connections: number } + currentUsage: { + requestsPerMinute: number; + requestsPerDay: number; + storageMB: number; + connections: number; + } ): { withinLimits: boolean; violations: string[] } { const violations: string[] = []; if (currentUsage.requestsPerMinute > limits.maxRequestsPerMinute) { - violations.push(`Requests per minute (${currentUsage.requestsPerMinute}) exceeds limit (${limits.maxRequestsPerMinute})`); + violations.push( + `Requests per minute (${currentUsage.requestsPerMinute}) exceeds limit (${limits.maxRequestsPerMinute})` + ); } if (currentUsage.requestsPerDay > limits.maxRequestsPerDay) { - violations.push(`Requests per day (${currentUsage.requestsPerDay}) exceeds limit (${limits.maxRequestsPerDay})`); + violations.push( + `Requests per day (${currentUsage.requestsPerDay}) exceeds limit (${limits.maxRequestsPerDay})` + ); } if (currentUsage.storageMB > limits.maxStorageMB) { - violations.push(`Storage (${currentUsage.storageMB}MB) exceeds limit (${limits.maxStorageMB}MB)`); + violations.push( + `Storage (${currentUsage.storageMB}MB) exceeds limit (${limits.maxStorageMB}MB)` + ); } if (currentUsage.connections > limits.maxConcurrentConnections) { - violations.push(`Connections (${currentUsage.connections}) exceeds limit (${limits.maxConcurrentConnections})`); + violations.push( + `Connections (${currentUsage.connections}) exceeds limit (${limits.maxConcurrentConnections})` + ); } return { @@ -73,7 +89,7 @@ export class SandboxUtils { } if (Array.isArray(data)) { - return data.map(item => this.sanitizeForSandbox(item)); + return data.map((item) => this.sanitizeForSandbox(item)); } if (typeof data === 'object' && data !== null) { @@ -116,7 +132,10 @@ export class SandboxUtils { }; } - static createSandboxHeaders(envId: string, additionalHeaders: Record = {}): Record { + static createSandboxHeaders( + envId: string, + additionalHeaders: Record = {} + ): Record { return { 'X-Sandbox-Environment': envId, 'X-Sandbox-Mode': 'true', @@ -125,7 +144,11 @@ export class SandboxUtils { }; } - static parseSandboxHeaders(headers: Record): { envId?: string; isSandbox: boolean; requestId?: string } { + static parseSandboxHeaders(headers: Record): { + envId?: string; + isSandbox: boolean; + requestId?: string; + } { return { envId: headers['X-Sandbox-Environment'], isSandbox: headers['X-Sandbox-Mode'] === 'true', diff --git a/sandbox/utils/testDataGenerator.ts b/sandbox/utils/testDataGenerator.ts index 09d1eff9..9a93cb3a 100644 --- a/sandbox/utils/testDataGenerator.ts +++ b/sandbox/utils/testDataGenerator.ts @@ -42,8 +42,28 @@ export class TestDataGenerator { const domains = ['gmail.com', 'yahoo.com', 'outlook.com', 'company.io']; for (let i = 0; i < count; i++) { - const firstName = this.randomFrom(['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry', 'Ivy', 'Jack']); - const lastName = this.randomFrom(['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis']); + const firstName = this.randomFrom([ + 'Alice', + 'Bob', + 'Charlie', + 'Diana', + 'Eve', + 'Frank', + 'Grace', + 'Henry', + 'Ivy', + 'Jack', + ]); + const lastName = this.randomFrom([ + 'Smith', + 'Johnson', + 'Williams', + 'Brown', + 'Jones', + 'Garcia', + 'Miller', + 'Davis', + ]); const domain = this.randomFrom(domains); users.push({ @@ -143,12 +163,15 @@ export class TestDataGenerator { return subscriptions; } - static generatePayments( - subscriptions: TestSubscription[], - count: number - ): TestPayment[] { + static generatePayments(subscriptions: TestSubscription[], count: number): TestPayment[] { const payments: TestPayment[] = []; - const statuses: TestPayment['status'][] = ['completed', 'completed', 'completed', 'pending', 'failed']; + const statuses: TestPayment['status'][] = [ + 'completed', + 'completed', + 'completed', + 'pending', + 'failed', + ]; for (let i = 0; i < count; i++) { const subscription = this.randomFrom(subscriptions); diff --git a/sdks/javascript/src/errors.ts b/sdks/javascript/src/errors.ts index 7a956dbc..bbbe4bf0 100644 --- a/sdks/javascript/src/errors.ts +++ b/sdks/javascript/src/errors.ts @@ -1,5 +1,9 @@ export class SubTrackrError extends Error { - constructor(public message: string, public statusCode?: number, public code?: string) { + constructor( + public message: string, + public statusCode?: number, + public code?: string + ) { super(message); this.name = 'SubTrackrError'; } diff --git a/src/animations/index.ts b/src/animations/index.ts index 9c306f0d..9a53bb9d 100644 --- a/src/animations/index.ts +++ b/src/animations/index.ts @@ -9,7 +9,11 @@ export * from '../hooks/useAnimationPerformance'; // Common animation components export * from '../components/common/SkeletonLoader'; -export * from '../components/common/SharedElement'; +export { + SharedElement, + SharedElementTransitionProvider, + type SharedElementProps, +} from '../components/common/SharedElement'; export * from '../components/common/ScreenTransitions'; export * from '../components/common/GestureAnimations'; diff --git a/src/components/UsageDashboard.tsx b/src/components/UsageDashboard.tsx index 16ef639b..dc72715a 100644 --- a/src/components/UsageDashboard.tsx +++ b/src/components/UsageDashboard.tsx @@ -5,7 +5,7 @@ export const UsageDashboard = () => { return ( Usage & Billing - + API Calls 85,000 / 100,000 diff --git a/src/components/common/ScreenTemplates.tsx b/src/components/common/ScreenTemplates.tsx index 7ac1a171..4420aa3f 100644 --- a/src/components/common/ScreenTemplates.tsx +++ b/src/components/common/ScreenTemplates.tsx @@ -111,7 +111,11 @@ export function ListScreen({ style={styles.scroll} refreshControl={ onRefresh ? ( - + ) : undefined }> {data.length === 0 ? ( @@ -123,7 +127,9 @@ export function ListScreen({ onAction={onEmptyAction} /> ) : ( - data.map((item, index) => {renderItem(item, index)}) + data.map((item, index) => ( + {renderItem(item, index)} + )) )} diff --git a/src/components/common/SwipeableCard.tsx b/src/components/common/SwipeableCard.tsx index 37eb9518..d8ddc559 100644 --- a/src/components/common/SwipeableCard.tsx +++ b/src/components/common/SwipeableCard.tsx @@ -182,7 +182,11 @@ const styles = StyleSheet.create({ marginBottom: spacing.md, }, actionBackground: { - ...StyleSheet.absoluteFillObject, + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', diff --git a/src/components/developer/DeveloperComponents.tsx b/src/components/developer/DeveloperComponents.tsx index b6a77561..c051b327 100644 --- a/src/components/developer/DeveloperComponents.tsx +++ b/src/components/developer/DeveloperComponents.tsx @@ -46,8 +46,8 @@ export const StatCard: React.FC = ({ label, value, trend, trendDi trendDirection === 'up' ? colors.success : trendDirection === 'down' - ? colors.error - : colors.textSecondary; + ? colors.error + : colors.textSecondary; return ( diff --git a/src/components/home/StatsCard.tsx b/src/components/home/StatsCard.tsx index 867f2901..4fea1b5f 100644 --- a/src/components/home/StatsCard.tsx +++ b/src/components/home/StatsCard.tsx @@ -10,14 +10,12 @@ interface StatsCardProps { currency?: string; } - export const StatsCard: React.FC = ({ totalMonthlySpend, totalActive, onWalletPress, currency = 'USD', }) => { - return ( {/* Monthly Spend Card - Primary Focus */} @@ -29,7 +27,7 @@ export const StatsCard: React.FC = ({ currency )}`}> Monthly Spend @@ -42,7 +40,6 @@ export const StatsCard: React.FC = ({ importantForAccessibility="no"> {formatCurrencyCompact(totalMonthlySpend, currency)} - {/* Active Count Card */} diff --git a/src/components/subscription/AnimatedSubscriptionCard.tsx b/src/components/subscription/AnimatedSubscriptionCard.tsx index 53b0a554..f6d8e27d 100644 --- a/src/components/subscription/AnimatedSubscriptionCard.tsx +++ b/src/components/subscription/AnimatedSubscriptionCard.tsx @@ -46,7 +46,6 @@ export const AnimatedSubscriptionCard: React.FC = const scaleAnim = useAnimatedValue(1); const priceAnim = useAnimatedValue(0); const statusAnim = useAnimatedValue(subscription.isActive ? 1 : 0); - const fallbackSharedElementAnim = useAnimatedValue(1); // Shared element transition const fallbackSharedElementAnim = useAnimatedValue(1); diff --git a/src/components/subscription/SubscriptionCard.tsx b/src/components/subscription/SubscriptionCard.tsx index accba0c4..b167bd7c 100644 --- a/src/components/subscription/SubscriptionCard.tsx +++ b/src/components/subscription/SubscriptionCard.tsx @@ -17,7 +17,6 @@ import { import { useSettingsStore } from '../../store/settingsStore'; import { currencyService } from '../../services/currencyService'; - export interface SubscriptionCardProps { subscription: Subscription; onPress: (subscription: Subscription) => void; @@ -50,7 +49,6 @@ export const SubscriptionCard: React.FC = React.memo( rates ); - return ( = React.memo( ]}> /{formatBillingCycle(subscription.billingCycle)} - diff --git a/src/config/features.ts b/src/config/features.ts index fcae8511..1bacff7d 100644 --- a/src/config/features.ts +++ b/src/config/features.ts @@ -198,7 +198,8 @@ export const FEATURE_CONFIG: FeatureConfig = { [FeatureId.DEVELOPER_PORTAL]: { id: FeatureId.DEVELOPER_PORTAL, name: 'Developer Portal', - description: 'Access the developer portal with API documentation, integration guides, and sandbox environment', + description: + 'Access the developer portal with API documentation, integration guides, and sandbox environment', enabled: true, tierAccess: [SubscriptionTier.ENTERPRISE], rolloutPercentage: 100, diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 6ef92076..94fcc9d1 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -20,7 +20,7 @@ import SlaDashboard from '../screens/SlaDashboard'; import GDPRSettingsScreen from '../screens/GDPRSettingsScreen'; import LanguageSettingsScreen from '../screens/LanguageSettingsScreen'; import SessionManagementScreen from '../screens/SessionManagementScreen'; -import SettingsScreen from '../screens/SettingsScreen'; +import { SettingsScreen } from '../screens/SettingsScreen'; import CalendarIntegrationScreen from '../screens/CalendarIntegrationScreen'; import AccountingExportScreen from '../screens/AccountingExportScreen'; import WebhookSettingsScreen from '../screens/WebhookSettingsScreen'; diff --git a/src/screens/AffiliateDashboardScreen.tsx b/src/screens/AffiliateDashboardScreen.tsx index c7095678..35e14f12 100644 --- a/src/screens/AffiliateDashboardScreen.tsx +++ b/src/screens/AffiliateDashboardScreen.tsx @@ -9,30 +9,21 @@ import { Alert, ActivityIndicator, Modal, - FlatList, } from 'react-native'; import { colors, spacing, typography, borderRadius } from '../utils/constants'; import { useAffiliateStore } from '../store/affiliateStore'; import { useWalletStore } from '../store/walletStore'; import { Card } from '../components/common/Card'; -import { useNavigation } from '@react-navigation/native'; -import { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { RootStackParamList } from '../navigation/types'; -import { AffiliateStatus, AffiliateProgram, Commission } from '../types/affiliate'; +import { AffiliateStatus } from '../types/affiliate'; const AffiliateDashboardScreen: React.FC = () => { - const navigation = useNavigation>(); const { affiliates, programs, commissions, metrics, isLoading, - error, registerAffiliate, - trackReferral, - calculateCommission, - payoutCommission, updateAffiliateStatus, getMetrics, } = useAffiliateStore(); @@ -60,22 +51,6 @@ const AffiliateDashboardScreen: React.FC = () => { Alert.alert('Success', 'You are now an affiliate!'); }, [address, selectedProgram, registerAffiliate]); - const handlePayout = useCallback( - async (affiliateId: string) => { - const affiliate = affiliates.find((a) => a.id === affiliateId); - if (!affiliate || affiliate.pendingPayout < affiliate.paymentThreshold) { - Alert.alert( - 'Minimum Threshold', - `You need at least $${affiliate?.paymentThreshold} to request a payout` - ); - return; - } - await payoutCommission(affiliateId); - Alert.alert('Success', 'Payout requested!'); - }, - [affiliates, payoutCommission] - ); - const handleToggleStatus = useCallback( async (affiliateId: string, newStatus: AffiliateStatus) => { await updateAffiliateStatus(affiliateId, newStatus); @@ -125,9 +100,7 @@ const AffiliateDashboardScreen: React.FC = () => { {affiliate.referrerAddress.slice(-4)} - - {affiliate.totalReferrals} referrals - + {affiliate.totalReferrals} referrals ${affiliate.totalEarnings.toFixed(2)} earned @@ -142,8 +115,8 @@ const AffiliateDashboardScreen: React.FC = () => { affiliate.status === AffiliateStatus.ACTIVE ? colors.success : affiliate.status === AffiliateStatus.PAUSED - ? colors.warning - : colors.danger, + ? colors.warning + : colors.error, }, ]}> {affiliate.status} @@ -151,17 +124,13 @@ const AffiliateDashboardScreen: React.FC = () => { {affiliate.status === AffiliateStatus.ACTIVE ? ( - handleToggleStatus(affiliate.id, AffiliateStatus.PAUSED) - }> + onPress={() => handleToggleStatus(affiliate.id, AffiliateStatus.PAUSED)}> Pause ) : ( - handleToggleStatus(affiliate.id, AffiliateStatus.ACTIVE) - }> + onPress={() => handleToggleStatus(affiliate.id, AffiliateStatus.ACTIVE)}> Resume )} @@ -192,8 +161,8 @@ const AffiliateDashboardScreen: React.FC = () => { {program.commissionConfig.type === 'percentage' ? `${program.commissionConfig.rate}%` : program.commissionConfig.type === 'flat' - ? `$${program.commissionConfig.rate}` - : 'Tiered'} + ? `$${program.commissionConfig.rate}` + : 'Tiered'} commission @@ -228,8 +197,8 @@ const AffiliateDashboardScreen: React.FC = () => { commission.status === 'paid' ? colors.success : commission.status === 'approved' - ? colors.warning - : colors.textSecondary, + ? colors.warning + : colors.textSecondary, }, ]}> {commission.status} @@ -257,9 +226,7 @@ const AffiliateDashboardScreen: React.FC = () => { Affiliate Dashboard - - Track referrals and earn commissions - + Track referrals and earn commissions {renderMetricsCard()} @@ -284,9 +251,7 @@ const AffiliateDashboardScreen: React.FC = () => { Select Program - - Choose an affiliate program to join - + Choose an affiliate program to join {programs.map((program) => ( { onPress={() => setSelectedProgram(program.id)}> {program.name} - - {program.description} - + {program.description} - {selectedProgram === program.id && ( - - )} + {selectedProgram === program.id && } ))} @@ -320,9 +281,7 @@ const AffiliateDashboardScreen: React.FC = () => { onPress={() => setProgramModalVisible(false)}> Cancel - + Join Program @@ -349,19 +308,19 @@ const styles = StyleSheet.create({ loadingText: { marginTop: spacing.sm, color: colors.textSecondary, - fontSize: typography.fontSizeMd, + fontSize: typography.body2.fontSize, }, header: { padding: spacing.md, paddingTop: spacing.lg, }, title: { - fontSize: typography.fontSizeXl, - fontWeight: typography.fontWeightBold, + fontSize: typography.h2.fontSize, + fontWeight: typography.h2.fontWeight, color: colors.text, }, subtitle: { - fontSize: typography.fontSizeMd, + fontSize: typography.body2.fontSize, color: colors.textSecondary, marginTop: spacing.xs, }, @@ -371,8 +330,8 @@ const styles = StyleSheet.create({ marginTop: 0, }, metricsTitle: { - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: '700', color: colors.text, marginBottom: spacing.md, }, @@ -390,12 +349,12 @@ const styles = StyleSheet.create({ borderRadius: borderRadius.md, }, metricValue: { - fontSize: typography.fontSizeLg, - fontWeight: typography.fontWeightBold, + fontSize: typography.h3.fontSize, + fontWeight: typography.h3.fontWeight, color: colors.text, }, metricLabel: { - fontSize: typography.fontSizeSm, + fontSize: typography.small.fontSize, color: colors.textSecondary, marginTop: spacing.xs, }, @@ -408,12 +367,12 @@ const styles = StyleSheet.create({ borderTopColor: colors.border, }, conversionLabel: { - fontSize: typography.fontSizeMd, + fontSize: typography.body2.fontSize, color: colors.textSecondary, }, conversionValue: { - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body2.fontSize, + fontWeight: '700', color: colors.primary, }, listCard: { @@ -422,13 +381,13 @@ const styles = StyleSheet.create({ marginTop: 0, }, listTitle: { - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: '700', color: colors.text, marginBottom: spacing.md, }, emptyText: { - fontSize: typography.fontSizeMd, + fontSize: typography.body2.fontSize, color: colors.textSecondary, textAlign: 'center', paddingVertical: spacing.lg, @@ -444,9 +403,8 @@ const styles = StyleSheet.create({ flex: 1, }, affiliateAddress: { - fontSize: typography.fontSizeMd, + fontSize: typography.body2.fontSize, color: colors.text, - fontFamily: typography.fontFamilyMono, }, affiliateStats: { flexDirection: 'row', @@ -454,7 +412,7 @@ const styles = StyleSheet.create({ gap: spacing.md, }, affiliateStat: { - fontSize: typography.fontSizeSm, + fontSize: typography.small.fontSize, color: colors.textSecondary, }, affiliateActions: { @@ -468,8 +426,8 @@ const styles = StyleSheet.create({ }, statusBadgeText: { color: colors.text, - fontSize: typography.fontSizeXs, - fontWeight: typography.fontWeightMedium, + fontSize: typography.small.fontSize, + fontWeight: '600', textTransform: 'capitalize', }, pauseButton: { @@ -481,7 +439,7 @@ const styles = StyleSheet.create({ }, pauseButtonText: { color: colors.warning, - fontSize: typography.fontSizeSm, + fontSize: typography.small.fontSize, }, resumeButton: { paddingHorizontal: spacing.sm, @@ -492,7 +450,7 @@ const styles = StyleSheet.create({ }, resumeButtonText: { color: colors.success, - fontSize: typography.fontSizeSm, + fontSize: typography.small.fontSize, }, programItem: { flexDirection: 'row', @@ -505,12 +463,12 @@ const styles = StyleSheet.create({ flex: 1, }, programName: { - fontSize: typography.fontSizeMd, + fontSize: typography.body.fontSize, color: colors.text, - fontWeight: typography.fontWeightMedium, + fontWeight: '600', }, programDescription: { - fontSize: typography.fontSizeSm, + fontSize: typography.body2.fontSize, color: colors.textSecondary, marginTop: spacing.xs, }, @@ -518,12 +476,12 @@ const styles = StyleSheet.create({ alignItems: 'flex-end', }, rateValue: { - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: '700', color: colors.primary, }, rateLabel: { - fontSize: typography.fontSizeXs, + fontSize: typography.small.fontSize, color: colors.textSecondary, }, commissionItem: { @@ -537,12 +495,11 @@ const styles = StyleSheet.create({ flex: 1, }, commissionId: { - fontSize: typography.fontSizeSm, + fontSize: typography.body2.fontSize, color: colors.text, - fontFamily: typography.fontFamilyMono, }, commissionDate: { - fontSize: typography.fontSizeXs, + fontSize: typography.small.fontSize, color: colors.textSecondary, marginTop: spacing.xs, }, @@ -550,8 +507,8 @@ const styles = StyleSheet.create({ alignItems: 'flex-end', }, amountValue: { - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: '700', color: colors.text, }, commissionStatus: { @@ -562,8 +519,8 @@ const styles = StyleSheet.create({ }, commissionStatusText: { color: colors.text, - fontSize: typography.fontSizeXs, - fontWeight: typography.fontWeightMedium, + fontSize: typography.small.fontSize, + fontWeight: '600', textTransform: 'capitalize', }, registerButton: { @@ -575,8 +532,8 @@ const styles = StyleSheet.create({ }, registerButtonText: { color: colors.text, - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: '700', }, modalOverlay: { flex: 1, @@ -591,13 +548,13 @@ const styles = StyleSheet.create({ maxHeight: '70%', }, modalTitle: { - fontSize: typography.fontSizeLg, - fontWeight: typography.fontWeightBold, + fontSize: typography.h3.fontSize, + fontWeight: '700', color: colors.text, marginBottom: spacing.xs, }, modalSubtitle: { - fontSize: typography.fontSizeMd, + fontSize: typography.body2.fontSize, color: colors.textSecondary, marginBottom: spacing.lg, }, @@ -617,12 +574,12 @@ const styles = StyleSheet.create({ flex: 1, }, programOptionName: { - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightMedium, + fontSize: typography.body.fontSize, + fontWeight: '600', color: colors.text, }, programOptionDesc: { - fontSize: typography.fontSizeSm, + fontSize: typography.body2.fontSize, color: colors.textSecondary, marginTop: spacing.xs, }, @@ -659,8 +616,8 @@ const styles = StyleSheet.create({ }, cancelButtonText: { color: colors.text, - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightMedium, + fontSize: typography.body.fontSize, + fontWeight: '600', }, confirmButton: { flex: 1, @@ -671,9 +628,9 @@ const styles = StyleSheet.create({ }, confirmButtonText: { color: colors.text, - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: '700', }, }); -export default AffiliateDashboardScreen; \ No newline at end of file +export default AffiliateDashboardScreen; diff --git a/src/screens/AnalyticsScreen.tsx b/src/screens/AnalyticsScreen.tsx index 25bd9c55..5ba4d7e9 100644 --- a/src/screens/AnalyticsScreen.tsx +++ b/src/screens/AnalyticsScreen.tsx @@ -18,7 +18,6 @@ import { currencyService } from '../services/currencyService'; import { calculateSubscriptionAnalytics } from '../services/analyticsService'; import { formatCurrency } from '../utils/formatting'; - const { width: screenWidth } = Dimensions.get('window'); const CHART_WIDTH = screenWidth - spacing.xl * 2; const CHART_HEIGHT = 200; @@ -34,7 +33,6 @@ const AnalyticsScreen: React.FC = () => { calculateStats(); }, [subscriptions, calculateStats, preferredCurrency, exchangeRates]); - const categoryData = useMemo(() => { const categories = Object.values(SubscriptionCategory); return categories @@ -103,7 +101,6 @@ const AnalyticsScreen: React.FC = () => { else if (sub.billingCycle === BillingCycle.YEARLY) total += priceInPreferred / 12; else if (sub.billingCycle === BillingCycle.WEEKLY) total += priceInPreferred * 4; } - } }); return { month, amount: total }; @@ -195,7 +192,6 @@ const AnalyticsScreen: React.FC = () => { importantForAccessibility="no"> {formatCurrency(stats.totalMonthlySpend, preferredCurrency)} - { importantForAccessibility="no"> {formatCurrency(stats.totalYearlySpend, preferredCurrency)} - @@ -301,7 +296,6 @@ const AnalyticsScreen: React.FC = () => { textAnchor="middle"> {formatCurrency(data.amount, preferredCurrency)} - )} ); @@ -389,7 +383,6 @@ const AnalyticsScreen: React.FC = () => { {formatCurrency(stats.totalYearlySpend, preferredCurrency)} - diff --git a/src/screens/ApiKeyManagementScreen.tsx b/src/screens/ApiKeyManagementScreen.tsx index 7be0fac3..4d198836 100644 --- a/src/screens/ApiKeyManagementScreen.tsx +++ b/src/screens/ApiKeyManagementScreen.tsx @@ -121,9 +121,7 @@ const ApiKeyManagementScreen: React.FC = () => { - - Environment: Sandbox (Development) - + Environment: Sandbox (Development) Keys are created for the current sandbox environment @@ -142,14 +140,10 @@ const ApiKeyManagementScreen: React.FC = () => { - handleCopyKey(showNewKey)}> + handleCopyKey(showNewKey)}> Copy Key - setShowNewKey(null)}> + setShowNewKey(null)}> Dismiss @@ -180,8 +174,8 @@ const ApiKeyManagementScreen: React.FC = () => { key.status === ApiKeyStatus.ACTIVE ? colors.success : key.status === ApiKeyStatus.REVOKED - ? colors.error - : colors.warning, + ? colors.error + : colors.warning, }, ]}> {key.status} @@ -192,16 +186,15 @@ const ApiKeyManagementScreen: React.FC = () => { - - {apiKeyService.maskApiKey(key.key)} - + {apiKeyService.maskApiKey(key.key)} Permissions: {(key.permissions ?? key.scopes ?? ['read']).join(', ')} - Rate: {key.rateLimit?.requestsPerMinute ?? 60}/min · {key.rateLimit?.requestsPerDay ?? 10000}/day + Rate: {key.rateLimit?.requestsPerMinute ?? 60}/min ·{' '} + {key.rateLimit?.requestsPerDay ?? 10000}/day {key.lastUsedAt && ( diff --git a/src/screens/CalendarIntegrationScreen.tsx b/src/screens/CalendarIntegrationScreen.tsx index 4db03dcd..a039fba6 100644 --- a/src/screens/CalendarIntegrationScreen.tsx +++ b/src/screens/CalendarIntegrationScreen.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useCallback } from 'react'; import { Alert, Linking, - Platform, SafeAreaView, ScrollView, Share, @@ -132,9 +131,15 @@ const CalendarIntegrationScreen: React.FC = () => { message: payload.ical, title: payload.filename, }); - Alert.alert('Calendar exported', `Exported ${payload.events.length} events to ${payload.filename}`); + Alert.alert( + 'Calendar exported', + `Exported ${payload.events.length} events to ${payload.filename}` + ); } catch (exportError) { - Alert.alert('Export failed', exportError instanceof Error ? exportError.message : 'Could not export calendar.'); + Alert.alert( + 'Export failed', + exportError instanceof Error ? exportError.message : 'Could not export calendar.' + ); } }, [subscriptions, timezone, exportCalendar]); @@ -424,13 +429,17 @@ const CalendarIntegrationScreen: React.FC = () => { Set your preferred timezone for calendar events. Current: {timezone}. - + {SUBSCRIPTION_TIMEZONES.map((tz) => ( setTimezone(tz)}> - + {tz} @@ -446,23 +455,26 @@ const CalendarIntegrationScreen: React.FC = () => { Check for conflicts - {scheduleConflicts.length > 0 ? ( - scheduleConflicts.slice(0, 5).map((conflict) => ( - - {conflict.date} - - {conflict.conflictingSubscriptions.length} subscriptions — {conflict.totalAmount.toFixed(2)} USD total - - {conflict.conflictingSubscriptions.map((sub) => ( - - {sub.name}: {sub.currency} {sub.amount.toFixed(2)} + {scheduleConflicts.length > 0 + ? scheduleConflicts.slice(0, 5).map((conflict) => ( + + {conflict.date} + + {conflict.conflictingSubscriptions.length} subscriptions —{' '} + {conflict.totalAmount.toFixed(2)} USD total - ))} - - )) - ) : scheduleConflicts.length === 0 && ( - No conflicts detected. Tap "Check for conflicts" to scan. - )} + {conflict.conflictingSubscriptions.map((sub) => ( + + {sub.name}: {sub.currency} {sub.amount.toFixed(2)} + + ))} + + )) + : scheduleConflicts.length === 0 && ( + + No conflicts detected. Tap "Check for conflicts" to scan. + + )} @@ -480,7 +492,9 @@ const CalendarIntegrationScreen: React.FC = () => { {payment.currency} {payment.amount.toFixed(2)} — {payment.status} - {new Date(payment.scheduledDate).toLocaleDateString()} + + {new Date(payment.scheduledDate).toLocaleDateString()} + {payment.status === 'pending' && ( { - const { - campaigns, - isLoading, - error, - createCampaign, - updateCampaign, - deleteCampaign, - launchCampaign, - pauseCampaign, - getCampaignAnalytics, - } = useCampaignStore(); + const { campaigns, isLoading, createCampaign, deleteCampaign, launchCampaign, pauseCampaign } = + useCampaignStore(); const [createModalVisible, setCreateModalVisible] = useState(false); const [newCampaign, setNewCampaign] = useState({ @@ -104,10 +88,7 @@ const CampaignManagementScreen: React.FC = () => { {campaign.name} + style={[styles.statusBadge, { backgroundColor: getStatusColor(campaign.status) }]}> {campaign.status} {getTypeLabel(campaign.type)} @@ -148,9 +129,7 @@ const CampaignManagementScreen: React.FC = () => { {analytics && ( - - {analytics.totalRecipients} - + {analytics.totalRecipients} Recipients @@ -281,8 +260,7 @@ const CampaignManagementScreen: React.FC = () => { key={channel} style={[ styles.channelOption, - newCampaign.channels.includes(channel) && - styles.channelOptionSelected, + newCampaign.channels.includes(channel) && styles.channelOptionSelected, ]} onPress={() => { const channels = newCampaign.channels.includes(channel) @@ -293,8 +271,7 @@ const CampaignManagementScreen: React.FC = () => { {channel} @@ -303,9 +280,7 @@ const CampaignManagementScreen: React.FC = () => { - + Create Campaign @@ -329,9 +304,7 @@ const CampaignManagementScreen: React.FC = () => { Campaign Management - - Create and manage marketing campaigns - + Create and manage marketing campaigns { {campaigns.length === 0 ? ( No campaigns yet - - Create your first campaign to get started - + Create your first campaign to get started ) : ( campaigns.map(renderCampaignItem) @@ -373,19 +344,19 @@ const styles = StyleSheet.create({ loadingText: { marginTop: spacing.sm, color: colors.textSecondary, - fontSize: typography.fontSizeMd, + fontSize: typography.body2.fontSize, }, header: { padding: spacing.md, paddingTop: spacing.lg, }, title: { - fontSize: typography.fontSizeXl, - fontWeight: typography.fontWeightBold, + fontSize: typography.h2.fontSize, + fontWeight: typography.h2.fontWeight, color: colors.text, }, subtitle: { - fontSize: typography.fontSizeMd, + fontSize: typography.body2.fontSize, color: colors.textSecondary, marginTop: spacing.xs, }, @@ -398,8 +369,8 @@ const styles = StyleSheet.create({ }, newButtonText: { color: colors.text, - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: '700', }, campaignCard: { padding: spacing.md, @@ -412,8 +383,8 @@ const styles = StyleSheet.create({ marginBottom: spacing.md, }, campaignName: { - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: '700', color: colors.text, }, campaignMeta: { @@ -429,12 +400,12 @@ const styles = StyleSheet.create({ }, statusBadgeText: { color: colors.text, - fontSize: typography.fontSizeXs, - fontWeight: typography.fontWeightMedium, + fontSize: typography.small.fontSize, + fontWeight: '600', textTransform: 'capitalize', }, campaignType: { - fontSize: typography.fontSizeSm, + fontSize: typography.body2.fontSize, color: colors.textSecondary, }, campaignActions: { @@ -449,8 +420,8 @@ const styles = StyleSheet.create({ }, launchButtonText: { color: colors.text, - fontSize: typography.fontSizeSm, - fontWeight: typography.fontWeightMedium, + fontSize: typography.small.fontSize, + fontWeight: '600', }, pauseButton: { paddingHorizontal: spacing.sm, @@ -460,19 +431,19 @@ const styles = StyleSheet.create({ }, pauseButtonText: { color: colors.text, - fontSize: typography.fontSizeSm, - fontWeight: typography.fontWeightMedium, + fontSize: typography.small.fontSize, + fontWeight: '600', }, deleteButton: { paddingHorizontal: spacing.sm, paddingVertical: spacing.xs, borderRadius: borderRadius.sm, borderWidth: 1, - borderColor: colors.danger, + borderColor: colors.error, }, deleteButtonText: { - color: colors.danger, - fontSize: typography.fontSizeSm, + color: colors.error, + fontSize: typography.small.fontSize, }, analyticsGrid: { flexDirection: 'row', @@ -486,12 +457,12 @@ const styles = StyleSheet.create({ alignItems: 'center', }, analyticsValue: { - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: '700', color: colors.text, }, analyticsLabel: { - fontSize: typography.fontSizeXs, + fontSize: typography.small.fontSize, color: colors.textSecondary, marginTop: spacing.xs, }, @@ -507,7 +478,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.background, }, channelBadgeText: { - fontSize: typography.fontSizeXs, + fontSize: typography.small.fontSize, color: colors.textSecondary, textTransform: 'uppercase', }, @@ -517,12 +488,12 @@ const styles = StyleSheet.create({ alignItems: 'center', }, emptyText: { - fontSize: typography.fontSizeMd, + fontSize: typography.body2.fontSize, color: colors.textSecondary, textAlign: 'center', }, emptySubtext: { - fontSize: typography.fontSizeSm, + fontSize: typography.small.fontSize, color: colors.textSecondary, textAlign: 'center', marginTop: spacing.xs, @@ -540,12 +511,12 @@ const styles = StyleSheet.create({ borderBottomColor: colors.border, }, closeButton: { - fontSize: typography.fontSizeMd, + fontSize: typography.body.fontSize, color: colors.primary, }, modalTitle: { - fontSize: typography.fontSizeLg, - fontWeight: typography.fontWeightBold, + fontSize: typography.h3.fontSize, + fontWeight: '700', color: colors.text, }, modalScroll: { @@ -556,7 +527,7 @@ const styles = StyleSheet.create({ marginBottom: spacing.lg, }, inputLabel: { - fontSize: typography.fontSizeSm, + fontSize: typography.body2.fontSize, color: colors.textSecondary, marginBottom: spacing.sm, }, @@ -564,7 +535,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.surface, borderRadius: borderRadius.md, padding: spacing.md, - fontSize: typography.fontSizeMd, + fontSize: typography.body.fontSize, color: colors.text, borderWidth: 1, borderColor: colors.border, @@ -591,11 +562,11 @@ const styles = StyleSheet.create({ borderColor: colors.primary, }, typeOptionText: { - fontSize: typography.fontSizeSm, + fontSize: typography.body2.fontSize, color: colors.text, }, typeOptionTextSelected: { - fontWeight: typography.fontWeightBold, + fontWeight: '700', }, channelGrid: { flexDirection: 'row', @@ -614,12 +585,12 @@ const styles = StyleSheet.create({ borderColor: colors.primary, }, channelOptionText: { - fontSize: typography.fontSizeSm, + fontSize: typography.body2.fontSize, color: colors.text, textTransform: 'uppercase', }, channelOptionTextSelected: { - fontWeight: typography.fontWeightBold, + fontWeight: '700', }, createButton: { backgroundColor: colors.primary, @@ -630,9 +601,9 @@ const styles = StyleSheet.create({ }, createButtonText: { color: colors.text, - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: '700', }, }); -export default CampaignManagementScreen; \ No newline at end of file +export default CampaignManagementScreen; diff --git a/src/screens/CancellationFlowScreen.tsx b/src/screens/CancellationFlowScreen.tsx index 96ad8112..371cc9cb 100644 --- a/src/screens/CancellationFlowScreen.tsx +++ b/src/screens/CancellationFlowScreen.tsx @@ -81,7 +81,9 @@ const CancellationFlowScreen: React.FC = ({ route, navigation }) => { {OFFER_TYPE_ICONS[offer.type] ?? '🎁'} {offer.title} - {offer.abVariant === 'A' ? 'Popular' : 'Best Value'} + + {offer.abVariant === 'A' ? 'Popular' : 'Best Value'} + {offer.description} diff --git a/src/screens/DeveloperPortalScreen.tsx b/src/screens/DeveloperPortalScreen.tsx index 87017952..3b41c791 100644 --- a/src/screens/DeveloperPortalScreen.tsx +++ b/src/screens/DeveloperPortalScreen.tsx @@ -54,7 +54,11 @@ const DeveloperPortalScreen: React.FC = () => { Alert.alert('Required fields', 'Name and email are required.'); return; } - await createDeveloperProfile(profileForm.name, profileForm.email, profileForm.company || undefined); + await createDeveloperProfile( + profileForm.name, + profileForm.email, + profileForm.company || undefined + ); await completeOnboardingStep(DeveloperOnboardingStep.CREATE_ACCOUNT); setShowOnboarding(false); }; @@ -63,7 +67,10 @@ const DeveloperPortalScreen: React.FC = () => { const name = newKeyName || 'Default Key'; try { const key = await generateApiKey(name); - Alert.alert('API Key Generated', `Your new API key:\n\n${key}\n\nCopy it now, it won't be shown again.`); + Alert.alert( + 'API Key Generated', + `Your new API key:\n\n${key}\n\nCopy it now, it won't be shown again.` + ); setNewKeyName(''); } catch { Alert.alert('Error', 'Failed to generate API key.'); @@ -161,31 +168,28 @@ const DeveloperPortalScreen: React.FC = () => { Developer Portal - - Welcome back, {developerProfile?.name || 'Developer'} - + Welcome back, {developerProfile?.name || 'Developer'} - {[SandboxEnvironment.DEVELOPMENT, SandboxEnvironment.STAGING, SandboxEnvironment.PRODUCTION].map( - (env) => ( - switchEnvironment(env)} - /> - ) - )} + {[ + SandboxEnvironment.DEVELOPMENT, + SandboxEnvironment.STAGING, + SandboxEnvironment.PRODUCTION, + ].map((env) => ( + switchEnvironment(env)} + /> + ))} - + { {guide.difficulty} · {guide.estimatedTime} - {guide.isCompleted && ( - - )} + {guide.isCompleted && } ))} @@ -330,8 +332,8 @@ const DeveloperPortalScreen: React.FC = () => { sub.status === 'active' ? colors.success : sub.status === 'paused' - ? colors.warning - : colors.error, + ? colors.warning + : colors.error, }, ]}> {sub.status} diff --git a/src/screens/DocumentationPortalScreen.tsx b/src/screens/DocumentationPortalScreen.tsx index de1ff3c0..0449256c 100644 --- a/src/screens/DocumentationPortalScreen.tsx +++ b/src/screens/DocumentationPortalScreen.tsx @@ -1,12 +1,5 @@ import React, { useState } from 'react'; -import { - View, - Text, - StyleSheet, - ScrollView, - SafeAreaView, - TouchableOpacity, -} from 'react-native'; +import { View, Text, StyleSheet, ScrollView, SafeAreaView, TouchableOpacity } from 'react-native'; import { Card } from '../components/common/Card'; import { colors, spacing, typography, borderRadius } from '../utils/constants'; @@ -28,7 +21,8 @@ const DOC_SECTIONS: DocSection[] = [ subsections: [ { title: 'Base URL', - content: 'Sandbox: https://api.sandbox.subtrackr.dev/v1\nProduction: https://api.subtrackr.dev/v1', + content: + 'Sandbox: https://api.sandbox.subtrackr.dev/v1\nProduction: https://api.subtrackr.dev/v1', }, { title: 'Authentication', @@ -37,8 +31,7 @@ const DOC_SECTIONS: DocSection[] = [ }, { title: 'Rate Limits', - content: - 'Sandbox: 60 requests/minute, 10,000 requests/day\nProduction: Varies by plan', + content: 'Sandbox: 60 requests/minute, 10,000 requests/day\nProduction: Varies by plan', }, ], }, @@ -50,7 +43,8 @@ const DOC_SECTIONS: DocSection[] = [ subsections: [ { title: 'GET /subscriptions', - content: 'List all subscriptions with optional filtering and pagination.\n\nQuery params: status, category, page, limit', + content: + 'List all subscriptions with optional filtering and pagination.\n\nQuery params: status, category, page, limit', }, { title: 'POST /subscriptions', @@ -146,12 +140,13 @@ const DOC_SECTIONS: DocSection[] = [ subsections: [ { title: 'Error Format', - content: 'All errors return: { "error": { "code": "string", "message": "string", "details": {} } }', + content: + 'All errors return: { "error": { "code": "string", "message": "string", "details": {} } }', }, { title: 'Common Error Codes', content: - '400 - Bad Request: Invalid parameters\n401 - Unauthorized: Invalid or missing API key\n403 - Forbidden: Insufficient permissions\n404 - Not Found: Resource doesn\'t exist\n429 - Rate Limited: Too many requests\n500 - Server Error: Internal error', + "400 - Bad Request: Invalid parameters\n401 - Unauthorized: Invalid or missing API key\n403 - Forbidden: Insufficient permissions\n404 - Not Found: Resource doesn't exist\n429 - Rate Limited: Too many requests\n500 - Server Error: Internal error", }, ], }, @@ -166,9 +161,7 @@ const DocumentationPortalScreen: React.FC = () => { API Documentation - - Complete reference for the SubTrackr API - + Complete reference for the SubTrackr API @@ -181,13 +174,12 @@ const DocumentationPortalScreen: React.FC = () => { setExpandedSection(expandedSection === section.id ? null : section.id)}> + onPress={() => + setExpandedSection(expandedSection === section.id ? null : section.id) + }> {section.icon} + style={[styles.tocText, expandedSection === section.id && styles.tocTextActive]}> {section.title} @@ -205,9 +197,7 @@ const DocumentationPortalScreen: React.FC = () => { {section.icon} {section.title} - - {expandedSection === section.id ? '▼' : '▶'} - + {expandedSection === section.id ? '▼' : '▶'} {expandedSection === section.id && ( diff --git a/src/screens/FraudDashboard.tsx b/src/screens/FraudDashboard.tsx index f26dd4b0..bb261eb6 100644 --- a/src/screens/FraudDashboard.tsx +++ b/src/screens/FraudDashboard.tsx @@ -73,15 +73,40 @@ const FraudDashboard: React.FC = () => { - {renderMetric('Total checks', analytics.totalChecks.toString(), 'Subscriptions reviewed', colors.accent)} - {renderMetric('Blocked', analytics.blocked.toString(), 'Automated hard stops', colors.error)} - {renderMetric('Flagged', analytics.flagged.toString(), 'Queued for review', colors.warning)} + {renderMetric( + 'Total checks', + analytics.totalChecks.toString(), + 'Subscriptions reviewed', + colors.accent + )} + {renderMetric( + 'Blocked', + analytics.blocked.toString(), + 'Automated hard stops', + colors.error + )} + {renderMetric( + 'Flagged', + analytics.flagged.toString(), + 'Queued for review', + colors.warning + )} {renderMetric('Avg risk', `${analytics.avgRisk}`, 'Aggregate risk score', colors.primary)} - {renderMetric('Velocity alerts', analytics.velocityAlerts.toString(), 'Rapid creation detected', colors.secondary)} - {renderMetric('Anomaly alerts', analytics.anomalyAlerts.toString(), 'Usage deviates from baseline', colors.accent)} + {renderMetric( + 'Velocity alerts', + analytics.velocityAlerts.toString(), + 'Rapid creation detected', + colors.secondary + )} + {renderMetric( + 'Anomaly alerts', + analytics.anomalyAlerts.toString(), + 'Usage deviates from baseline', + colors.accent + )} {renderMetric( 'Chargeback predictions', analytics.chargebackPredictions.toString(), @@ -121,19 +146,27 @@ const FraudDashboard: React.FC = () => { - approveSubscription(item.subscriptionId)}> + approveSubscription(item.subscriptionId)}> Approve - resolveCase(item.subscriptionId, 'flag')}> + resolveCase(item.subscriptionId, 'flag')}> Flag - blockSubscription(item.subscriptionId)}> + blockSubscription(item.subscriptionId)}> Block ))} - {reviewQueue.length === 0 ? No cases awaiting manual review. : null} + {reviewQueue.length === 0 ? ( + No cases awaiting manual review. + ) : null} @@ -181,7 +214,8 @@ const FraudDashboard: React.FC = () => { {report.averageRisk} - {report.totalSubscriptions} subs · {report.flaggedSubscriptions} flagged · {report.blockedSubscriptions} blocked + {report.totalSubscriptions} subs · {report.flaggedSubscriptions} flagged ·{' '} + {report.blockedSubscriptions} blocked Manual review {report.manualReviewCount} @@ -201,21 +235,45 @@ const FraudDashboard: React.FC = () => { Approved - + {analytics.approved} Flagged - + {analytics.flagged} Blocked - + {analytics.blocked} diff --git a/src/screens/GroupManagementScreen.tsx b/src/screens/GroupManagementScreen.tsx index 4b094d05..a40cf754 100644 --- a/src/screens/GroupManagementScreen.tsx +++ b/src/screens/GroupManagementScreen.tsx @@ -39,7 +39,9 @@ const GroupManagementScreen: React.FC = () => { Seats: {analytics?.activeSeats ?? group.members.length}/{group.planSharingRules.seatLimit} - Outstanding: ${analytics?.outstandingBalance.toFixed(2) ?? '0.00'} + + Outstanding: ${analytics?.outstandingBalance.toFixed(2) ?? '0.00'} +