Skip to content

Commit 32a5236

Browse files
authored
Merge pull request #119 from Junman140/feature/billing-push-notifications
Feature/billing-push-notifications
2 parents cdf6f1e + 35f5e9f commit 32a5236

14 files changed

Lines changed: 689 additions & 118 deletions

App.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import { StatusBar } from 'expo-status-bar';
33
import { AppNavigator } from './src/navigation/AppNavigator';
4+
import { useNotifications } from './src/hooks/useNotifications';
45

56
// Import WalletConnect compatibility layer
67
import '@walletconnect/react-native-compat';
@@ -61,10 +62,16 @@ createAppKit({
6162
enableAnalytics: true,
6263
});
6364

65+
function NotificationBootstrap() {
66+
useNotifications();
67+
return null;
68+
}
69+
6470
export default function App() {
6571
return (
6672
<>
6773
<StatusBar style="light" />
74+
<NotificationBootstrap />
6875
<AppNavigator />
6976
<AppKit />
7077
</>

app.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@
3939
"owner": "subtrackr",
4040
"description": "Subscription Management with Crypto Payments",
4141
"keywords": ["subscription", "crypto", "web3", "blockchain", "payments"],
42-
"primaryColor": "#6366f1"
42+
"primaryColor": "#6366f1",
43+
"plugins": [
44+
[
45+
"expo-notifications",
46+
{
47+
"sounds": [],
48+
"enableBackgroundRemoteNotifications": false
49+
}
50+
]
51+
]
4352
}
4453
}

package-lock.json

Lines changed: 82 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"ethers": "^5.8.0",
4242
"expo": "~53.0.20",
4343
"expo-application": "~6.1.5",
44+
"expo-notifications": "^0.31.5",
4445
"expo-status-bar": "~2.2.3",
4546
"graphql": "^16.13.1",
4647
"react": "19.0.0",

src/hooks/useNotifications.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { useEffect, useState, useCallback } from 'react';
2+
import AsyncStorage from '@react-native-async-storage/async-storage';
3+
import * as Notifications from 'expo-notifications';
4+
5+
import { useSubscriptionStore } from '../store';
6+
import {
7+
attachNotificationResponseListeners,
8+
getPermissionStatus,
9+
requestNotificationPermissions,
10+
syncRenewalReminders,
11+
navigateToSubscriptionFromNotification,
12+
} from '../services/notificationService';
13+
14+
const FIRST_LAUNCH_PROMPT_KEY = '@subtrackr_notifications_prompt_v1';
15+
16+
export function useNotifications(): {
17+
permissionStatus: Notifications.PermissionStatus | null;
18+
refreshPermission: () => Promise<void>;
19+
} {
20+
const subscriptions = useSubscriptionStore((s) => s.subscriptions);
21+
const [permissionStatus, setPermissionStatus] = useState<Notifications.PermissionStatus | null>(
22+
null
23+
);
24+
25+
const refreshPermission = useCallback(async () => {
26+
setPermissionStatus(await getPermissionStatus());
27+
}, []);
28+
29+
useEffect(() => {
30+
void refreshPermission();
31+
}, [refreshPermission]);
32+
33+
/** First app launch: ask for notification permission once, then keep schedules in sync. */
34+
useEffect(() => {
35+
let cancelled = false;
36+
37+
(async () => {
38+
const alreadyPrompted = await AsyncStorage.getItem(FIRST_LAUNCH_PROMPT_KEY);
39+
if (!alreadyPrompted) {
40+
await requestNotificationPermissions();
41+
if (!cancelled) {
42+
await AsyncStorage.setItem(FIRST_LAUNCH_PROMPT_KEY, '1');
43+
}
44+
} else {
45+
await getPermissionStatus();
46+
}
47+
48+
if (!cancelled) {
49+
await refreshPermission();
50+
}
51+
})();
52+
53+
return () => {
54+
cancelled = true;
55+
};
56+
}, []);
57+
58+
useEffect(() => {
59+
void syncRenewalReminders(subscriptions);
60+
}, [subscriptions]);
61+
62+
useEffect(() => {
63+
const remove = attachNotificationResponseListeners();
64+
65+
void Notifications.getLastNotificationResponseAsync().then((response) => {
66+
if (!response) return;
67+
const data = response.notification.request.content.data as
68+
| { subscriptionId?: string }
69+
| undefined;
70+
if (data?.subscriptionId) {
71+
navigateToSubscriptionFromNotification(data.subscriptionId);
72+
}
73+
});
74+
75+
return remove;
76+
}, []);
77+
78+
return { permissionStatus, refreshPermission };
79+
}

src/navigation/AppNavigator.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import { Text } from 'react-native';
33
import { NavigationContainer } from '@react-navigation/native';
4+
import { navigationRef } from './navigationRef';
45
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
56
import { createNativeStackNavigator } from '@react-navigation/native-stack';
67
import HomeScreen from '../screens/HomeScreen';
@@ -109,7 +110,7 @@ const TabNavigator = () => (
109110

110111
export const AppNavigator = () => {
111112
return (
112-
<NavigationContainer>
113+
<NavigationContainer ref={navigationRef}>
113114
<TabNavigator />
114115
</NavigationContainer>
115116
);

src/navigation/navigationRef.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { createNavigationContainerRef } from '@react-navigation/native';
2+
3+
import type { TabParamList } from './types';
4+
5+
export const navigationRef = createNavigationContainerRef<TabParamList>();

src/navigation/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { NavigatorScreenParams } from '@react-navigation/native';
2+
13
export type RootStackParamList = {
24
Home: undefined;
35
AddSubscription: undefined;
@@ -9,7 +11,7 @@ export type RootStackParamList = {
911
};
1012

1113
export type TabParamList = {
12-
HomeTab: undefined;
14+
HomeTab: NavigatorScreenParams<RootStackParamList> | undefined;
1315
AddTab: undefined;
1416
WalletTab: undefined;
1517
AnalyticsTab: undefined;

0 commit comments

Comments
 (0)