Skip to content

Commit 0c250ba

Browse files
committed
Merge main into 284
2 parents 0d4cced + de0682d commit 0c250ba

109 files changed

Lines changed: 20082 additions & 3172 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.prettierrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
"tabWidth": 2,
99
"useTabs": false,
1010
"printWidth": 100,
11-
"endOfLine": "lf"
11+
"endOfLine": "auto"
1212
}

app/tests/integration/contract-store.integration.test.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { act } from 'react';
99
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
1010
import AsyncStorage from '@react-native-async-storage/async-storage';
11+
import * as notificationService from '../../../src/services/notificationService';
1112
import { useSubscriptionStore } from '../../../src/store/subscriptionStore';
1213
import { SubscriptionCategory, BillingCycle } from '../../../src/types/subscription';
1314
import { makeSubscription, makeSubscriptionFormData, resetIdCounter } from './factories';
@@ -32,16 +33,17 @@ jest.mock('@react-native-async-storage/async-storage', () => ({
3233
}));
3334

3435
// ── Notification service mock (the "contract" side) ───────────────────────────
35-
const mockSyncRenewalReminders = jest.fn(() => Promise.resolve());
36-
const mockPresentChargeSuccess = jest.fn(() => Promise.resolve());
37-
const mockPresentChargeFailed = jest.fn(() => Promise.resolve());
38-
3936
jest.mock('../../../src/services/notificationService', () => ({
40-
syncRenewalReminders: (...args: unknown[]) => mockSyncRenewalReminders(...args),
41-
presentChargeSuccessNotification: (...args: unknown[]) => mockPresentChargeSuccess(...args),
42-
presentChargeFailedNotification: (...args: unknown[]) => mockPresentChargeFailed(...args),
37+
syncRenewalReminders: jest.fn(() => Promise.resolve()),
38+
presentChargeSuccessNotification: jest.fn(() => Promise.resolve()),
39+
presentChargeFailedNotification: jest.fn(() => Promise.resolve()),
40+
presentLocalNotification: jest.fn(() => Promise.resolve()),
4341
}));
4442

43+
const mockSyncRenewalReminders = notificationService.syncRenewalReminders as jest.Mock;
44+
const mockPresentChargeSuccess = notificationService.presentChargeSuccessNotification as jest.Mock;
45+
const mockPresentChargeFailed = notificationService.presentChargeFailedNotification as jest.Mock;
46+
4547
// ── Helpers ───────────────────────────────────────────────────────────────────
4648
function resetStore() {
4749
useSubscriptionStore.setState({
@@ -83,7 +85,7 @@ describe('contract-store integration', () => {
8385
});
8486

8587
expect(mockSyncRenewalReminders).toHaveBeenCalledTimes(1);
86-
const [subs] = mockSyncRenewalReminders.mock.calls[0] as [unknown[]];
88+
const [subs] = mockSyncRenewalReminders.mock.calls[0] as unknown as [unknown[]];
8789
expect(Array.isArray(subs)).toBe(true);
8890
expect((subs as { name: string }[]).some((s) => s.name === 'GitHub Copilot')).toBe(true);
8991
});
@@ -101,7 +103,7 @@ describe('contract-store integration', () => {
101103
});
102104

103105
expect(mockSyncRenewalReminders).toHaveBeenCalledTimes(1);
104-
const [subs] = mockSyncRenewalReminders.mock.calls[0] as [{ price: number }[]];
106+
const [subs] = mockSyncRenewalReminders.mock.calls[0] as unknown as [{ price: number }[]];
105107
expect(subs[0].price).toBe(19.99);
106108
});
107109

@@ -116,7 +118,7 @@ describe('contract-store integration', () => {
116118
});
117119

118120
expect(mockSyncRenewalReminders).toHaveBeenCalledTimes(1);
119-
const [subs] = mockSyncRenewalReminders.mock.calls[0] as [{ name: string }[]];
121+
const [subs] = mockSyncRenewalReminders.mock.calls[0] as unknown as [{ name: string }[]];
120122
expect(subs.every((s) => s.name !== 'Remove Me')).toBe(true);
121123
});
122124

app/tests/integration/notification-delivery.integration.test.ts

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,32 +25,28 @@ jest.mock('react-native', () => ({
2525
}));
2626

2727
// ── expo-notifications mock ───────────────────────────────────────────────────
28-
const mockSchedule = jest.fn(() => Promise.resolve('notif-id'));
29-
const mockGetScheduled = jest.fn(() => Promise.resolve([]));
30-
const mockCancel = jest.fn(() => Promise.resolve());
31-
const mockGetPermissions = jest.fn(() =>
32-
Promise.resolve({ status: Notifications.PermissionStatus.GRANTED })
33-
);
34-
const mockRequestPermissions = jest.fn(() =>
35-
Promise.resolve({ status: Notifications.PermissionStatus.GRANTED })
36-
);
37-
const mockSetHandler = jest.fn();
38-
const mockSetChannel = jest.fn(() => Promise.resolve());
39-
4028
jest.mock('expo-notifications', () => ({
4129
PermissionStatus: { GRANTED: 'granted', DENIED: 'denied', UNDETERMINED: 'undetermined' },
4230
AndroidImportance: { HIGH: 4 },
4331
AndroidNotificationVisibility: { PUBLIC: 1 },
4432
SchedulableTriggerInputTypes: { DATE: 'date' },
45-
setNotificationHandler: (...args: unknown[]) => mockSetHandler(...args),
46-
setNotificationChannelAsync: (...args: unknown[]) => mockSetChannel(...args),
47-
getPermissionsAsync: () => mockGetPermissions(),
48-
requestPermissionsAsync: () => mockRequestPermissions(),
49-
scheduleNotificationAsync: (...args: unknown[]) => mockSchedule(...args),
50-
getAllScheduledNotificationsAsync: () => mockGetScheduled(),
51-
cancelScheduledNotificationAsync: (...args: unknown[]) => mockCancel(...args),
33+
setNotificationHandler: jest.fn(),
34+
setNotificationChannelAsync: jest.fn(() => Promise.resolve()),
35+
getPermissionsAsync: jest.fn(() => Promise.resolve({ status: 'granted' })),
36+
requestPermissionsAsync: jest.fn(() => Promise.resolve({ status: 'granted' })),
37+
scheduleNotificationAsync: jest.fn(() => Promise.resolve('notif-id')),
38+
getAllScheduledNotificationsAsync: jest.fn(() => Promise.resolve([])),
39+
cancelScheduledNotificationAsync: jest.fn(() => Promise.resolve()),
5240
}));
5341

42+
const mockSchedule = Notifications.scheduleNotificationAsync as jest.Mock;
43+
const mockGetScheduled = Notifications.getAllScheduledNotificationsAsync as jest.Mock;
44+
const mockCancel = Notifications.cancelScheduledNotificationAsync as jest.Mock;
45+
const mockGetPermissions = Notifications.getPermissionsAsync as jest.Mock;
46+
const mockRequestPermissions = Notifications.requestPermissionsAsync as jest.Mock;
47+
const mockSetHandler = Notifications.setNotificationHandler as jest.Mock;
48+
const mockSetChannel = Notifications.setNotificationChannelAsync as jest.Mock;
49+
5450
beforeEach(() => {
5551
mockSchedule.mockClear();
5652
mockGetScheduled.mockClear();
@@ -77,7 +73,7 @@ describe('notification delivery integration', () => {
7773
await presentChargeSuccessNotification(sub);
7874

7975
expect(mockSchedule).toHaveBeenCalledTimes(1);
80-
const [payload] = mockSchedule.mock.calls[0] as [
76+
const [payload] = mockSchedule.mock.calls[0] as unknown as [
8177
{ content: { title: string; data: { type: string } }; trigger: null },
8278
];
8379
expect(payload.content.title).toContain('Linear');
@@ -90,7 +86,7 @@ describe('notification delivery integration', () => {
9086
await presentChargeFailedNotification(sub);
9187

9288
expect(mockSchedule).toHaveBeenCalledTimes(1);
93-
const [payload] = mockSchedule.mock.calls[0] as [
89+
const [payload] = mockSchedule.mock.calls[0] as unknown as [
9490
{ content: { title: string; data: { type: string } }; trigger: null },
9591
];
9692
expect(payload.content.title).toContain('Figma');
@@ -102,15 +98,15 @@ describe('notification delivery integration', () => {
10298
const sub = makeSubscription();
10399
await presentChargeFailedNotification(sub, 'Insufficient balance');
104100

105-
const [payload] = mockSchedule.mock.calls[0] as [{ content: { body: string } }];
101+
const [payload] = mockSchedule.mock.calls[0] as unknown as [{ content: { body: string } }];
106102
expect(payload.content.body).toBe('Insufficient balance');
107103
});
108104

109105
it('presentTransactionQueueNotification schedules with correct type', async () => {
110106
await presentTransactionQueueNotification('Queue update', 'Your transaction was processed');
111107

112108
expect(mockSchedule).toHaveBeenCalledTimes(1);
113-
const [payload] = mockSchedule.mock.calls[0] as [
109+
const [payload] = mockSchedule.mock.calls[0] as unknown as [
114110
{ content: { title: string; body: string; data: { type: string } } },
115111
];
116112
expect(payload.content.title).toBe('Queue update');
@@ -128,7 +124,7 @@ describe('notification delivery integration', () => {
128124
},
129125
trigger: null,
130126
};
131-
mockGetScheduled.mockResolvedValueOnce([existingNotif]);
127+
mockGetScheduled.mockResolvedValueOnce([existingNotif] as never);
132128

133129
const sub = makeSubscription({
134130
nextBillingDate: new Date(Date.now() + 48 * 60 * 60 * 1000), // 2 days from now
@@ -142,7 +138,7 @@ describe('notification delivery integration', () => {
142138
});
143139

144140
it('syncRenewalReminders does not schedule for inactive subscriptions', async () => {
145-
mockGetScheduled.mockResolvedValueOnce([]);
141+
mockGetScheduled.mockResolvedValueOnce([] as unknown as never);
146142

147143
const sub = makeSubscription({ isActive: false, notificationsEnabled: true });
148144
await syncRenewalReminders([sub]);
@@ -151,7 +147,7 @@ describe('notification delivery integration', () => {
151147
});
152148

153149
it('syncRenewalReminders does not schedule when notificationsEnabled is false', async () => {
154-
mockGetScheduled.mockResolvedValueOnce([]);
150+
mockGetScheduled.mockResolvedValueOnce([] as unknown as never);
155151

156152
const sub = makeSubscription({
157153
isActive: true,
@@ -164,7 +160,7 @@ describe('notification delivery integration', () => {
164160
});
165161

166162
it('syncRenewalReminders schedules a reminder for an active sub with future billing date', async () => {
167-
mockGetScheduled.mockResolvedValueOnce([]);
163+
mockGetScheduled.mockResolvedValueOnce([] as unknown as never);
168164

169165
const sub = makeSubscription({
170166
isActive: true,
@@ -176,7 +172,9 @@ describe('notification delivery integration', () => {
176172
await syncRenewalReminders([sub]);
177173

178174
expect(mockSchedule).toHaveBeenCalledTimes(1);
179-
const [payload] = mockSchedule.mock.calls[0] as [{ content: { data: { type: string } } }];
175+
const [payload] = mockSchedule.mock.calls[0] as unknown as [
176+
{ content: { data: { type: string } } },
177+
];
180178
expect(payload.content.data.type).toBe(NOTIFICATION_DATA_TYPE.RENEWAL_REMINDER);
181179
});
182180

backend/services/auditService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export class AuditService {
145145
applyRetention(): number {
146146
const cutoff = Date.now() - this.retention.maxAgeMs;
147147
const before = this.log.length;
148-
this.log = this.log.filter((e) => e.timestamp >= cutoff);
148+
this.log = this.log.filter((e) => e.timestamp > cutoff);
149149
return before - this.log.length; // number of events pruned
150150
}
151151

backend/services/gdpr.ts

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,13 @@ export interface UserConsent {
1313

1414
export const exportUserData = async (userId: string) => {
1515
console.log(`Exporting data for user: ${userId}`);
16-
16+
1717
// In a real scenario, this would query multiple tables/collections
1818
const userData = {
1919
profile: { id: userId, email: 'user@example.com', registeredAt: '2026-01-01' },
20-
subscriptions: [
21-
{ id: 'sub_1', name: 'Netflix', amount: 15.99, status: 'active' }
22-
],
23-
billingHistory: [
24-
{ id: 'tx_1', date: '2026-04-20', amount: 15.99, status: 'completed' }
25-
],
26-
consentLogs: [
27-
{ type: 'analytics', status: 'granted', date: '2026-01-01' }
28-
],
20+
subscriptions: [{ id: 'sub_1', name: 'Netflix', amount: 15.99, status: 'active' }],
21+
billingHistory: [{ id: 'tx_1', date: '2026-04-20', amount: 15.99, status: 'completed' }],
22+
consentLogs: [{ type: 'analytics', status: 'granted', date: '2026-01-01' }],
2923
};
3024

3125
return JSON.stringify(userData, null, 2);
@@ -42,23 +36,15 @@ export const deleteUserData = async (userId: string, permanent: boolean = false)
4236
// Hard delete logic across all services
4337
// await SubscriptionModel.deleteMany({ userId });
4438
// await ProfileModel.deleteOne({ userId });
45-
39+
4640
return { success: true, message: 'User data permanently deleted' };
4741
};
4842

4943
export const anonymizeUserData = async (userId: string) => {
5044
console.log(`Anonymizing data for user: ${userId}`);
5145

52-
// Replace sensitive identifiers with null/dummy values
53-
const updates = {
54-
email: `deleted-${Date.now()}@anonymized.invalid`,
55-
name: 'Anonymized User',
56-
address: null,
57-
phone: null,
58-
};
59-
6046
// await ProfileModel.updateOne({ userId }, updates);
61-
47+
6248
return { success: true, message: 'User data has been anonymized' };
6349
};
6450

@@ -70,8 +56,8 @@ export const updateConsent = async (userId: string, preferences: Partial<UserCon
7056

7157
// Log consent change for audit trail
7258
console.log(`Consent updated for ${userId}:`, newConsent);
73-
59+
7460
// await ConsentAuditModel.create({ userId, ...newConsent });
75-
61+
7662
return newConsent;
7763
};

contracts/Cargo.toml

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,3 @@
1-
[package]
2-
name = "subtrackr-subscription"
3-
version = "0.2.0"
4-
edition = "2021"
5-
authors = ["SubTrackr Team"]
6-
description = "SubTrackr subscription implementation contract (Soroban)"
7-
8-
[lib]
9-
crate-type = ["cdylib", "rlib"]
10-
11-
[dependencies]
12-
soroban-sdk = "21.0.0"
13-
subtrackr-types = { path = "../types" }
14-
15-
[dev-dependencies]
16-
soroban-sdk = { version = "21.0.0", features = ["testutils"] }
17-
arbitrary = { version = "1.3", features = ["derive"] }
181
[workspace]
192
resolver = "2"
203
members = [

contracts/proxy/src/lib.rs

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#![no_std]
2+
#![allow(clippy::too_many_arguments)]
23

34
mod storage;
45

@@ -13,11 +14,7 @@ fn current_proxy_address(env: &Env) -> Address {
1314
env.current_contract_address()
1415
}
1516

16-
fn invoke_impl<T: TryFromVal<Env, Val>>(
17-
env: &Env,
18-
func: &str,
19-
args: Vec<Val>,
20-
) -> T {
17+
fn invoke_impl<T: TryFromVal<Env, Val>>(env: &Env, func: &str, args: Vec<Val>) -> T {
2118
let impl_addr = proxy_storage::implementation(env);
2219
env.invoke_contract(&impl_addr, &soroban_sdk::Symbol::new(env, func), args)
2320
}
@@ -92,11 +89,7 @@ impl UpgradeableProxy {
9289
let target_version: u32 = env.invoke_contract(
9390
&implementation,
9491
&soroban_sdk::Symbol::new(&env, "get_version"),
95-
soroban_sdk::vec![
96-
&env,
97-
proxy_addr.into_val(&env),
98-
storage.into_val(&env)
99-
],
92+
soroban_sdk::vec![&env, proxy_addr.into_val(&env), storage.into_val(&env)],
10093
);
10194
proxy_storage::set_version(&env, target_version);
10295
}
@@ -133,13 +126,13 @@ impl UpgradeableProxy {
133126
// Basic interface validation: ensure new implementation supports expected interface.
134127
let proxy_addr = current_proxy_address(&env);
135128
let storage_addr = proxy_storage::storage_address(&env);
136-
let args: Vec<Val> = soroban_sdk::vec![
137-
&env,
138-
proxy_addr.into_val(&env),
139-
storage_addr.into_val(&env)
140-
];
141-
let _target_version: u32 =
142-
env.invoke_contract(&implementation, &soroban_sdk::Symbol::new(&env, "get_version"), args);
129+
let args: Vec<Val> =
130+
soroban_sdk::vec![&env, proxy_addr.into_val(&env), storage_addr.into_val(&env)];
131+
let _target_version: u32 = env.invoke_contract(
132+
&implementation,
133+
&soroban_sdk::Symbol::new(&env, "get_version"),
134+
args,
135+
);
143136

144137
proxy_storage::set_scheduled_upgrade(
145138
&env,
@@ -194,7 +187,10 @@ impl UpgradeableProxy {
194187
);
195188

196189
let now = env.ledger().timestamp();
197-
assert!(now >= scheduled.execute_after, "Upgrade timelock not expired");
190+
assert!(
191+
now >= scheduled.execute_after,
192+
"Upgrade timelock not expired"
193+
);
198194

199195
let proxy_addr = current_proxy_address(&env);
200196
let storage_addr = proxy_storage::storage_address(&env);
@@ -471,12 +467,7 @@ impl UpgradeableProxy {
471467
);
472468
}
473469

474-
pub fn pause_by_subscriber(
475-
env: Env,
476-
subscriber: Address,
477-
subscription_id: u64,
478-
duration: u64,
479-
) {
470+
pub fn pause_by_subscriber(env: Env, subscriber: Address, subscription_id: u64, duration: u64) {
480471
let proxy_addr = current_proxy_address(&env);
481472
let storage_addr = proxy_storage::storage_address(&env);
482473
invoke_impl::<()>(

0 commit comments

Comments
 (0)