From 11c01962736a33aab80163a60b4bd7bc5ddf9ee2 Mon Sep 17 00:00:00 2001 From: Nathan_akin <85641756+akintewe@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:44:47 +0100 Subject: [PATCH 1/5] integrated notifications --- apps/mobile/app.json | 11 +++++- apps/mobile/src/app/App.tsx | 17 ++++++++- .../components/PrivateMessages/Chat/index.tsx | 19 ++++++++-- apps/mobile/src/services/notifications.ts | 16 +++++++++ .../src/services/notifications/index.ts | 4 +++ .../notifications/pushNotification.ts | 28 +++++++++++++++ .../services/notifications/registration.ts | 35 +++++++++++++++++++ .../src/services/notifications/types.ts | 15 ++++++++ apps/mobile/src/utils/notifications.ts | 16 +++++++++ 9 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 apps/mobile/src/services/notifications.ts create mode 100644 apps/mobile/src/services/notifications/index.ts create mode 100644 apps/mobile/src/services/notifications/pushNotification.ts create mode 100644 apps/mobile/src/services/notifications/registration.ts create mode 100644 apps/mobile/src/services/notifications/types.ts create mode 100644 apps/mobile/src/utils/notifications.ts diff --git a/apps/mobile/app.json b/apps/mobile/app.json index 4b3774eb9..03b891c24 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -37,7 +37,8 @@ "android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.INTERNET", "android.permission.RECORD_AUDIO", - "android.permission.CAMERA" + "android.permission.CAMERA", + "NOTIFICATIONS" ] }, "web": { @@ -88,6 +89,14 @@ "merchantIdentifier": "", "enableGooglePay": true } + ], + [ + "expo-notifications", + { + "icon": "./assets/notification-icon.png", + "color": "#ffffff", + "sounds": ["./assets/notification-sound.wav"] + } ] ], "sdkVersion": "51.0.0", diff --git a/apps/mobile/src/app/App.tsx b/apps/mobile/src/app/App.tsx index b7f12378f..ca2b0e515 100644 --- a/apps/mobile/src/app/App.tsx +++ b/apps/mobile/src/app/App.tsx @@ -4,11 +4,13 @@ import {starknetChainId, useAccount} from '@starknet-react/core'; import * as Font from 'expo-font'; import * as SplashScreen from 'expo-splash-screen'; import {useCallback, useEffect, useState} from 'react'; -import {View} from 'react-native'; +import {View, Platform} from 'react-native'; import {useTips} from '../hooks'; import {useDialog, useToast} from '../hooks/modals'; import {Router} from './Router'; +import {registerForPushNotificationsAsync} from '../services/notifications'; +import * as Notifications from 'expo-notifications'; SplashScreen.preventAutoHideAsync(); @@ -81,6 +83,19 @@ export default function App() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [tips.data]); + useEffect(() => { + if (Platform.OS !== 'web') { + registerForPushNotificationsAsync() + .then(token => { + if (token) { + console.log('Push token:', token); + // Store token in your state management system + } + }) + .catch(err => console.error('Failed to get push token:', err)); + } + }, []); + const onLayoutRootView = useCallback(async () => { if (appIsReady) { await SplashScreen.hideAsync(); diff --git a/apps/mobile/src/components/PrivateMessages/Chat/index.tsx b/apps/mobile/src/components/PrivateMessages/Chat/index.tsx index 4ea346486..280123f94 100644 --- a/apps/mobile/src/components/PrivateMessages/Chat/index.tsx +++ b/apps/mobile/src/components/PrivateMessages/Chat/index.tsx @@ -8,6 +8,8 @@ import {useToast} from '../../../hooks/modals'; import {IconButton} from '../../IconButton'; import {MessageInput} from '../PrivateMessageInput'; import stylesheet from './styles'; +import { sendNotificationForEvent } from '../../../utils/notifications'; + export type ChatProps = { item: { @@ -51,11 +53,24 @@ export const Chat: React.FC = ({item, handleGoBack, user}) => { receiverPublicKeyProps: receiverPublicKey, }, { - onSuccess: () => { - // showToast({title: 'Message sent', type: 'success'}); + onSuccess: async () => { queryClient.invalidateQueries({ queryKey: ['messagesSent'], }); + + try { + await sendNotificationForEvent( + receiverPublicKey, + 'privateMessage', + { + senderName: user?.name || 'Someone', + conversationId: item.id.toString(), + authorName: message.substring(0, 50) + (message.length > 50 ? '...' : '') + } + ); + } catch (error) { + console.error('Failed to send notification:', error); + } }, onError() { showToast({title: 'Error sending message', type: 'error'}); diff --git a/apps/mobile/src/services/notifications.ts b/apps/mobile/src/services/notifications.ts new file mode 100644 index 000000000..f0759e4f5 --- /dev/null +++ b/apps/mobile/src/services/notifications.ts @@ -0,0 +1,16 @@ +export type NotificationData = { + type: 'tip' | 'privateMessage' | 'tokenLaunch' | 'note' | 'tokenLiquidity'; + data: { + amount?: string; + token?: string; + senderName?: string; + tokenName?: string; + action?: string; + authorName?: string; + conversationId?: string; + coinAddress?: string; + noteId?: string; + liquidityAmount?: string; + message?: string; + }; +}; \ No newline at end of file diff --git a/apps/mobile/src/services/notifications/index.ts b/apps/mobile/src/services/notifications/index.ts new file mode 100644 index 000000000..fd48194e1 --- /dev/null +++ b/apps/mobile/src/services/notifications/index.ts @@ -0,0 +1,4 @@ +export * from './types'; +export * from './pushNotification'; +export * from './registration'; +export * from './handler'; \ No newline at end of file diff --git a/apps/mobile/src/services/notifications/pushNotification.ts b/apps/mobile/src/services/notifications/pushNotification.ts new file mode 100644 index 000000000..2982a9e78 --- /dev/null +++ b/apps/mobile/src/services/notifications/pushNotification.ts @@ -0,0 +1,28 @@ +import * as Device from 'expo-device'; +import * as Notifications from 'expo-notifications'; +import {Platform} from 'react-native'; +import Constants from 'expo-constants'; +import {NotificationData} from './types'; + +export async function sendPushNotification( + expoPushToken: string, + title: string, + body: string, + data?: NotificationData +) { + const message = { + to: expoPushToken, + sound: 'default', + title, + body, + data: data || {}, + }; + + await fetch('https://exp.host/--/api/v2/push/send', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(message), + }); +} \ No newline at end of file diff --git a/apps/mobile/src/services/notifications/registration.ts b/apps/mobile/src/services/notifications/registration.ts new file mode 100644 index 000000000..2bfc6e986 --- /dev/null +++ b/apps/mobile/src/services/notifications/registration.ts @@ -0,0 +1,35 @@ +import * as Device from 'expo-device'; +import * as Notifications from 'expo-notifications'; +import {Platform} from 'react-native'; +import Constants from 'expo-constants'; + +export async function registerForPushNotificationsAsync() { + if (Platform.OS === 'web') return null; + + if (!Device.isDevice) return null; + + const {status: existingStatus} = await Notifications.getPermissionsAsync(); + let finalStatus = existingStatus; + + if (existingStatus !== 'granted') { + const {status} = await Notifications.requestPermissionsAsync(); + finalStatus = status; + } + + if (finalStatus !== 'granted') return null; + + const token = await Notifications.getExpoPushTokenAsync({ + projectId: Constants.expoConfig?.extra?.eas?.projectId + }); + + if (Platform.OS === 'android') { + await Notifications.setNotificationChannelAsync('default', { + name: 'default', + importance: Notifications.AndroidImportance.MAX, + vibrationPattern: [0, 250, 250, 250], + lightColor: '#FF231F7C', + }); + } + + return token.data; +} \ No newline at end of file diff --git a/apps/mobile/src/services/notifications/types.ts b/apps/mobile/src/services/notifications/types.ts new file mode 100644 index 000000000..fc03df2d2 --- /dev/null +++ b/apps/mobile/src/services/notifications/types.ts @@ -0,0 +1,15 @@ +export type NotificationData = { + type: 'tip' | 'privateMessage' | 'tokenLaunch' | 'note' | 'tokenLiquidity'; + data: { + amount?: string; + token?: string; + senderName?: string; + tokenName?: string; + action?: string; + authorName?: string; + conversationId?: string; + coinAddress?: string; + noteId?: string; + liquidityAmount?: string; + }; +}; \ No newline at end of file diff --git a/apps/mobile/src/utils/notifications.ts b/apps/mobile/src/utils/notifications.ts new file mode 100644 index 000000000..9f96d1f55 --- /dev/null +++ b/apps/mobile/src/utils/notifications.ts @@ -0,0 +1,16 @@ +import {NotificationData} from '../services/notifications/types'; + +export const sendNotificationForEvent = async ( + receiverPublicKey: string, + type: NotificationData['type'], + data: NotificationData['data'] +) => { + try { + // Implementation for sending notifications + // This is a placeholder - actual implementation would depend on your notification service + console.log('Sending notification:', {receiverPublicKey, type, data}); + } catch (error) { + console.error('Error sending notification:', error); + throw error; + } +}; \ No newline at end of file From 3e44e2819c7e0031c6f0156a4f43f51bb0f4a864 Mon Sep 17 00:00:00 2001 From: Nathan_akin <85641756+akintewe@users.noreply.github.com> Date: Sat, 2 Nov 2024 19:17:47 +0100 Subject: [PATCH 2/5] integrated notifications --- apps/mobile/package.json | 8 ++++---- apps/mobile/src/app/App.tsx | 9 ++++----- .../components/PrivateMessages/Chat/index.tsx | 19 +++++++------------ apps/mobile/src/services/notifications.ts | 2 +- .../src/services/notifications/index.ts | 4 ++-- .../notifications/pushNotification.ts | 8 ++------ .../services/notifications/registration.ts | 14 +++++++------- .../src/services/notifications/types.ts | 2 +- apps/mobile/src/utils/notifications.ts | 4 ++-- 9 files changed, 30 insertions(+), 40 deletions(-) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 6dd4f1177..ef688c3df 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -35,7 +35,7 @@ "@dynamic-labs/client": "4.0.0-alpha.8", "@dynamic-labs/react-hooks": "4.0.0-alpha.8", "@dynamic-labs/react-native-extension": "4.0.0-alpha.8", - "@dynamic-labs/utils": "^3.3.0", + "@dynamic-labs/utils": "4.0.0-alpha.8", "@dynamic-labs/viem-extension": "4.0.0-alpha.8", "@expo/metro-runtime": "~3.2.1", "@getalby/bitcoin-connect-react": "^3.5.3", @@ -45,7 +45,7 @@ "@nostr-dev-kit/ndk": "2.10.1", "@nostr-dev-kit/ndk-wallet": "^0.2.0", "@rainbow-me/rainbowkit": "^2.1.5", - "@react-native-async-storage/async-storage": "^1.24.0", + "@react-native-async-storage/async-storage": "1.23.1", "@react-native-community/netinfo": "11.3.1", "@react-native-picker/picker": "2.7.5", "@react-navigation/bottom-tabs": "^6.5.20", @@ -72,7 +72,7 @@ "common": "workspace:*", "crypto-es": "^2.1.0", "events": "^3.3.0", - "expo": "~51.0.28", + "expo": "~51.0.38", "expo-application": "^5.9.1", "expo-auth-session": "^5.5.2", "expo-av": "~14.0.7", @@ -127,7 +127,7 @@ "viem": "2.x", "wagmi": "^2.12.8", "zustand": "^4.5.2", - "@stripe/stripe-react-native":"0.38.6" + "@stripe/stripe-react-native": "0.37.2" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/apps/mobile/src/app/App.tsx b/apps/mobile/src/app/App.tsx index ca2b0e515..e2b847c44 100644 --- a/apps/mobile/src/app/App.tsx +++ b/apps/mobile/src/app/App.tsx @@ -4,13 +4,12 @@ import {starknetChainId, useAccount} from '@starknet-react/core'; import * as Font from 'expo-font'; import * as SplashScreen from 'expo-splash-screen'; import {useCallback, useEffect, useState} from 'react'; -import {View, Platform} from 'react-native'; +import {Platform, View} from 'react-native'; import {useTips} from '../hooks'; import {useDialog, useToast} from '../hooks/modals'; -import {Router} from './Router'; import {registerForPushNotificationsAsync} from '../services/notifications'; -import * as Notifications from 'expo-notifications'; +import {Router} from './Router'; SplashScreen.preventAutoHideAsync(); @@ -86,13 +85,13 @@ export default function App() { useEffect(() => { if (Platform.OS !== 'web') { registerForPushNotificationsAsync() - .then(token => { + .then((token) => { if (token) { console.log('Push token:', token); // Store token in your state management system } }) - .catch(err => console.error('Failed to get push token:', err)); + .catch((err) => console.error('Failed to get push token:', err)); } }, []); diff --git a/apps/mobile/src/components/PrivateMessages/Chat/index.tsx b/apps/mobile/src/components/PrivateMessages/Chat/index.tsx index 280123f94..1d0ee7bb9 100644 --- a/apps/mobile/src/components/PrivateMessages/Chat/index.tsx +++ b/apps/mobile/src/components/PrivateMessages/Chat/index.tsx @@ -5,11 +5,10 @@ import {FlatList, Image, Text, View} from 'react-native'; import {useStyles} from '../../../hooks'; import {useToast} from '../../../hooks/modals'; +import {sendNotificationForEvent} from '../../../utils/notifications'; import {IconButton} from '../../IconButton'; import {MessageInput} from '../PrivateMessageInput'; import stylesheet from './styles'; -import { sendNotificationForEvent } from '../../../utils/notifications'; - export type ChatProps = { item: { @@ -57,17 +56,13 @@ export const Chat: React.FC = ({item, handleGoBack, user}) => { queryClient.invalidateQueries({ queryKey: ['messagesSent'], }); - + try { - await sendNotificationForEvent( - receiverPublicKey, - 'privateMessage', - { - senderName: user?.name || 'Someone', - conversationId: item.id.toString(), - authorName: message.substring(0, 50) + (message.length > 50 ? '...' : '') - } - ); + await sendNotificationForEvent(receiverPublicKey, 'privateMessage', { + senderName: user?.name || 'Someone', + conversationId: item.id.toString(), + authorName: message.substring(0, 50) + (message.length > 50 ? '...' : ''), + }); } catch (error) { console.error('Failed to send notification:', error); } diff --git a/apps/mobile/src/services/notifications.ts b/apps/mobile/src/services/notifications.ts index f0759e4f5..7625152e4 100644 --- a/apps/mobile/src/services/notifications.ts +++ b/apps/mobile/src/services/notifications.ts @@ -13,4 +13,4 @@ export type NotificationData = { liquidityAmount?: string; message?: string; }; -}; \ No newline at end of file +}; diff --git a/apps/mobile/src/services/notifications/index.ts b/apps/mobile/src/services/notifications/index.ts index fd48194e1..9efa302cf 100644 --- a/apps/mobile/src/services/notifications/index.ts +++ b/apps/mobile/src/services/notifications/index.ts @@ -1,4 +1,4 @@ -export * from './types'; +export * from './handler'; export * from './pushNotification'; export * from './registration'; -export * from './handler'; \ No newline at end of file +export * from './types'; diff --git a/apps/mobile/src/services/notifications/pushNotification.ts b/apps/mobile/src/services/notifications/pushNotification.ts index 2982a9e78..006a5bf4e 100644 --- a/apps/mobile/src/services/notifications/pushNotification.ts +++ b/apps/mobile/src/services/notifications/pushNotification.ts @@ -1,14 +1,10 @@ -import * as Device from 'expo-device'; -import * as Notifications from 'expo-notifications'; -import {Platform} from 'react-native'; -import Constants from 'expo-constants'; import {NotificationData} from './types'; export async function sendPushNotification( expoPushToken: string, title: string, body: string, - data?: NotificationData + data?: NotificationData, ) { const message = { to: expoPushToken, @@ -25,4 +21,4 @@ export async function sendPushNotification( }, body: JSON.stringify(message), }); -} \ No newline at end of file +} diff --git a/apps/mobile/src/services/notifications/registration.ts b/apps/mobile/src/services/notifications/registration.ts index 2bfc6e986..7e974efcd 100644 --- a/apps/mobile/src/services/notifications/registration.ts +++ b/apps/mobile/src/services/notifications/registration.ts @@ -1,25 +1,25 @@ +import Constants from 'expo-constants'; import * as Device from 'expo-device'; import * as Notifications from 'expo-notifications'; import {Platform} from 'react-native'; -import Constants from 'expo-constants'; export async function registerForPushNotificationsAsync() { if (Platform.OS === 'web') return null; - + if (!Device.isDevice) return null; const {status: existingStatus} = await Notifications.getPermissionsAsync(); let finalStatus = existingStatus; - + if (existingStatus !== 'granted') { const {status} = await Notifications.requestPermissionsAsync(); finalStatus = status; } - + if (finalStatus !== 'granted') return null; - + const token = await Notifications.getExpoPushTokenAsync({ - projectId: Constants.expoConfig?.extra?.eas?.projectId + projectId: Constants.expoConfig?.extra?.eas?.projectId, }); if (Platform.OS === 'android') { @@ -32,4 +32,4 @@ export async function registerForPushNotificationsAsync() { } return token.data; -} \ No newline at end of file +} diff --git a/apps/mobile/src/services/notifications/types.ts b/apps/mobile/src/services/notifications/types.ts index fc03df2d2..ed2d3dc6f 100644 --- a/apps/mobile/src/services/notifications/types.ts +++ b/apps/mobile/src/services/notifications/types.ts @@ -12,4 +12,4 @@ export type NotificationData = { noteId?: string; liquidityAmount?: string; }; -}; \ No newline at end of file +}; diff --git a/apps/mobile/src/utils/notifications.ts b/apps/mobile/src/utils/notifications.ts index 9f96d1f55..42fb38866 100644 --- a/apps/mobile/src/utils/notifications.ts +++ b/apps/mobile/src/utils/notifications.ts @@ -3,7 +3,7 @@ import {NotificationData} from '../services/notifications/types'; export const sendNotificationForEvent = async ( receiverPublicKey: string, type: NotificationData['type'], - data: NotificationData['data'] + data: NotificationData['data'], ) => { try { // Implementation for sending notifications @@ -13,4 +13,4 @@ export const sendNotificationForEvent = async ( console.error('Error sending notification:', error); throw error; } -}; \ No newline at end of file +}; From 817c2e6386918da804c86c4fd07b9cfdf56a60f1 Mon Sep 17 00:00:00 2001 From: Nathan_akin <85641756+akintewe@users.noreply.github.com> Date: Sat, 2 Nov 2024 20:14:16 +0100 Subject: [PATCH 3/5] integrated notifications --- apps/mobile/index.js | 3 +-- apps/mobile/src/components/TabSelector/index.tsx | 2 +- apps/mobile/src/hooks/launchpad/useCreateToken.ts | 8 ++++---- apps/mobile/src/modules/Lightning/index.tsx | 4 ++-- apps/mobile/src/modules/PostCard/index.tsx | 2 +- apps/mobile/src/modules/VideoPostCard/index.tsx | 2 +- apps/mobile/src/modules/WalletModal/walletUtil.ts | 4 ++-- 7 files changed, 12 insertions(+), 13 deletions(-) diff --git a/apps/mobile/index.js b/apps/mobile/index.js index 4480d8fba..c86437ebc 100644 --- a/apps/mobile/index.js +++ b/apps/mobile/index.js @@ -9,5 +9,4 @@ import registerRootComponent from 'expo/build/launch/registerRootComponent'; import {Wrapper} from './src/app/Wrapper'; registerRootComponent(Wrapper); - -AppRegistry.registerComponent(appName, () => App); \ No newline at end of file +AppRegistry.registerComponent(appName, () => App); diff --git a/apps/mobile/src/components/TabSelector/index.tsx b/apps/mobile/src/components/TabSelector/index.tsx index 55eed5f48..863173eef 100644 --- a/apps/mobile/src/components/TabSelector/index.tsx +++ b/apps/mobile/src/components/TabSelector/index.tsx @@ -43,7 +43,7 @@ const TabSelector: React.FC = ({ key={i} style={[ tabStyle ?? styles.tab, - activeTab === b?.tab ? activeTabStyle ?? styles.active : null, + activeTab === b?.tab ? (activeTabStyle ?? styles.active) : null, ]} onPress={() => handlePress(b?.tab, b?.screen)} > diff --git a/apps/mobile/src/hooks/launchpad/useCreateToken.ts b/apps/mobile/src/hooks/launchpad/useCreateToken.ts index 512810b22..f43c2181c 100644 --- a/apps/mobile/src/hooks/launchpad/useCreateToken.ts +++ b/apps/mobile/src/hooks/launchpad/useCreateToken.ts @@ -15,8 +15,8 @@ export type DeployTokenFormValues = { export const useCreateToken = () => { const deployToken = async (account: AccountInterface, data: DeployTokenFormValues) => { const CONTRACT_ADDRESS_SALT_DEFAULT = - data?.contract_address_salt ?? - (await account?.getChainId()) == constants.StarknetChainId.SN_MAIN + (data?.contract_address_salt ?? + (await account?.getChainId()) == constants.StarknetChainId.SN_MAIN) ? '0x36d8be2991d685af817ef9d127ffb00fbb98a88d910195b04ec4559289a99f6' : '0x36d8be2991d685af817ef9d127ffb00fbb98a88d910195b04ec4559289a99f6'; @@ -51,8 +51,8 @@ export const useCreateToken = () => { const deployTokenAndLaunch = async (account: AccountInterface, data: DeployTokenFormValues) => { const CONTRACT_ADDRESS_SALT_DEFAULT = - data?.contract_address_salt ?? - (await account?.getChainId()) == constants.StarknetChainId.SN_MAIN + (data?.contract_address_salt ?? + (await account?.getChainId()) == constants.StarknetChainId.SN_MAIN) ? '0x36d8be2991d685af817ef9d127ffb00fbb98a88d910195b04ec4559289a99f6' : '0x36d8be2991d685af817ef9d127ffb00fbb98a88d910195b04ec4559289a99f6'; diff --git a/apps/mobile/src/modules/Lightning/index.tsx b/apps/mobile/src/modules/Lightning/index.tsx index 192c2a129..d31ef2e69 100644 --- a/apps/mobile/src/modules/Lightning/index.tsx +++ b/apps/mobile/src/modules/Lightning/index.tsx @@ -241,8 +241,8 @@ export const LightningNetworkWallet = () => { {isLoading ? 'Connecting...' : isExtensionAvailable - ? 'Connect with Alby Extension' - : 'Connect with Alby NWC'} + ? 'Connect with Alby Extension' + : 'Connect with Alby NWC'} diff --git a/apps/mobile/src/modules/PostCard/index.tsx b/apps/mobile/src/modules/PostCard/index.tsx index b5388a195..e67a91a74 100644 --- a/apps/mobile/src/modules/PostCard/index.tsx +++ b/apps/mobile/src/modules/PostCard/index.tsx @@ -20,7 +20,7 @@ export const PostCard: React.FC = ({event, isRepostProps, isBookm let repostedEvent = undefined; const [isRepost, setIsRepost] = useState( - isRepostProps ?? event?.kind == NDKKind.Repost ? true : false, + (isRepostProps ?? event?.kind == NDKKind.Repost) ? true : false, ); if (event?.kind == NDKKind.Repost) { diff --git a/apps/mobile/src/modules/VideoPostCard/index.tsx b/apps/mobile/src/modules/VideoPostCard/index.tsx index b503bfdeb..832166dcb 100644 --- a/apps/mobile/src/modules/VideoPostCard/index.tsx +++ b/apps/mobile/src/modules/VideoPostCard/index.tsx @@ -20,7 +20,7 @@ export const VideoPostCard: React.FC = ({event, isRepostProps, is let repostedEvent = undefined; const [isRepost, setIsRepost] = useState( - isRepostProps ?? event?.kind == NDKKind.Repost ? true : false, + (isRepostProps ?? event?.kind == NDKKind.Repost) ? true : false, ); if (event?.kind == NDKKind.Repost) { diff --git a/apps/mobile/src/modules/WalletModal/walletUtil.ts b/apps/mobile/src/modules/WalletModal/walletUtil.ts index 59d3780fd..53285c9a4 100644 --- a/apps/mobile/src/modules/WalletModal/walletUtil.ts +++ b/apps/mobile/src/modules/WalletModal/walletUtil.ts @@ -79,8 +79,8 @@ export function injectedWithFallback() { return !window.ethereum ? 'Install MetaMask' : window.ethereum?.isMetaMask - ? 'MetaMask' - : 'Browser Wallet'; + ? 'MetaMask' + : 'Browser Wallet'; }, }; }); From 37c511cb928784f04ce3499bd7f68b69abaad53f Mon Sep 17 00:00:00 2001 From: Nathan_akin <85641756+akintewe@users.noreply.github.com> Date: Sat, 2 Nov 2024 22:18:44 +0100 Subject: [PATCH 4/5] integrated notifications --- apps/mobile/app.json | 8 +- apps/mobile/src/app/App.tsx | 53 +------------- apps/mobile/src/serviceWorker.js | 19 +++++ apps/mobile/src/services/notifications.ts | 5 ++ .../src/services/notifications/handler.ts | 2 + .../src/services/notifications/index.ts | 3 +- .../notifications/webNotifications.ts | 16 ++++ .../notifications/webPushNotification.ts | 26 +++++++ apps/mobile/src/utils/notifications.ts | 11 +++ apps/pwa/.expo/README.md | 8 ++ apps/pwa/.expo/devices.json | 3 + apps/pwa/package.json | 8 +- apps/pwa/public/service-worker.js | 19 +++++ apps/pwa/src/app/layout.tsx | 8 +- apps/pwa/src/app/providers.tsx | 73 ++++--------------- .../app/providers/NotificationProvider.tsx | 31 ++++++++ apps/pwa/src/app/wagmiConfig.ts | 31 ++++++++ apps/pwa/src/services/notifications.ts | 32 ++++++++ apps/pwa/tsconfig.json | 14 +++- 19 files changed, 248 insertions(+), 122 deletions(-) create mode 100644 apps/mobile/src/serviceWorker.js create mode 100644 apps/mobile/src/services/notifications/handler.ts create mode 100644 apps/mobile/src/services/notifications/webNotifications.ts create mode 100644 apps/mobile/src/services/notifications/webPushNotification.ts create mode 100644 apps/pwa/.expo/README.md create mode 100644 apps/pwa/.expo/devices.json create mode 100644 apps/pwa/public/service-worker.js create mode 100644 apps/pwa/src/app/providers/NotificationProvider.tsx create mode 100644 apps/pwa/src/app/wagmiConfig.ts create mode 100644 apps/pwa/src/services/notifications.ts diff --git a/apps/mobile/app.json b/apps/mobile/app.json index 03b891c24..b91912e29 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -43,8 +43,12 @@ }, "web": { "favicon": "./assets/favicon.png", - "deepLinking": true, - "bundler": "metro" + "bundler": "metro", + "fonts": [ + { + "asset": "./assets/fonts/PublicPixel-z84yD.ttf" + } + ] }, "plugins": [ [ diff --git a/apps/mobile/src/app/App.tsx b/apps/mobile/src/app/App.tsx index e2b847c44..07c98ba57 100644 --- a/apps/mobile/src/app/App.tsx +++ b/apps/mobile/src/app/App.tsx @@ -1,14 +1,12 @@ import '@walletconnect/react-native-compat'; - import {starknetChainId, useAccount} from '@starknet-react/core'; import * as Font from 'expo-font'; import * as SplashScreen from 'expo-splash-screen'; import {useCallback, useEffect, useState} from 'react'; import {Platform, View} from 'react-native'; - +import {registerForPushNotificationsAsync} from '../services/notifications'; import {useTips} from '../hooks'; import {useDialog, useToast} from '../hooks/modals'; -import {registerForPushNotificationsAsync} from '../services/notifications'; import {Router} from './Router'; SplashScreen.preventAutoHideAsync(); @@ -16,7 +14,6 @@ SplashScreen.preventAutoHideAsync(); export default function App() { const [appIsReady, setAppIsReady] = useState(false); const [sentTipNotification, setSentTipNotification] = useState(false); - const tips = useTips(); const {showToast} = useToast(); @@ -39,59 +36,15 @@ export default function App() { })(); }, []); - const {showDialog, hideDialog} = useDialog(); - - const account = useAccount(); - - useEffect(() => { - const chainId = account.chainId ? starknetChainId(account.chainId) : undefined; - - if (chainId) { - // if (chainId !== CHAIN_ID) { - // showDialog({ - // title: 'Wrong Network', - // description: - // 'AFK currently only supports the Starknet Sepolia network. Please switch to the Sepolia network to continue.', - // buttons: [], - // }); - // } else { - // hideDialog(); - // } - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [account.chainId]); - - useEffect(() => { - const interval = setInterval(() => tips.refetch(), 2 * 60 * 1_000); - return () => clearInterval(interval); - }, [tips]); - - useEffect(() => { - if (sentTipNotification) return; - - const hasUnclaimedTip = (tips.data ?? []).some((tip) => !tip.claimed && tip.depositId); - if (hasUnclaimedTip) { - setSentTipNotification(true); - showToast({ - type: 'info', - title: 'You have unclaimed tips', - }); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tips.data]); - useEffect(() => { if (Platform.OS !== 'web') { registerForPushNotificationsAsync() - .then((token) => { + .then((token: string | null) => { if (token) { console.log('Push token:', token); - // Store token in your state management system } }) - .catch((err) => console.error('Failed to get push token:', err)); + .catch((error: Error) => console.error('Failed to get push token:', error)); } }, []); diff --git a/apps/mobile/src/serviceWorker.js b/apps/mobile/src/serviceWorker.js new file mode 100644 index 000000000..567b7371c --- /dev/null +++ b/apps/mobile/src/serviceWorker.js @@ -0,0 +1,19 @@ +self.addEventListener('push', event => { + const options = { + body: event.data.text(), + icon: '/notification-icon.png', + badge: '/badge-icon.png', + vibrate: [200, 100, 200] + }; + + event.waitUntil( + self.registration.showNotification('AFK Community', options) + ); +}); + +self.addEventListener('notificationclick', event => { + event.notification.close(); + event.waitUntil( + clients.openWindow('/') + ); +}); \ No newline at end of file diff --git a/apps/mobile/src/services/notifications.ts b/apps/mobile/src/services/notifications.ts index 7625152e4..a8d936ec9 100644 --- a/apps/mobile/src/services/notifications.ts +++ b/apps/mobile/src/services/notifications.ts @@ -14,3 +14,8 @@ export type NotificationData = { message?: string; }; }; + +export const registerForPushNotificationsAsync = async () => { + // Implementation here + return null; +}; diff --git a/apps/mobile/src/services/notifications/handler.ts b/apps/mobile/src/services/notifications/handler.ts new file mode 100644 index 000000000..9e1644a1b --- /dev/null +++ b/apps/mobile/src/services/notifications/handler.ts @@ -0,0 +1,2 @@ +// Add your handler-related code here +export {}; \ No newline at end of file diff --git a/apps/mobile/src/services/notifications/index.ts b/apps/mobile/src/services/notifications/index.ts index 9efa302cf..90f41aeb8 100644 --- a/apps/mobile/src/services/notifications/index.ts +++ b/apps/mobile/src/services/notifications/index.ts @@ -1,4 +1,5 @@ -export * from './handler'; + export * from './pushNotification'; export * from './registration'; export * from './types'; +export * from './webNotifications'; diff --git a/apps/mobile/src/services/notifications/webNotifications.ts b/apps/mobile/src/services/notifications/webNotifications.ts new file mode 100644 index 000000000..ade26ae9a --- /dev/null +++ b/apps/mobile/src/services/notifications/webNotifications.ts @@ -0,0 +1,16 @@ +import { Platform } from "react-native"; + +export const setupWebNotifications = async () => { + if (Platform.OS !== 'web') return; + + if (!('Notification' in window)) return; + + try { + const permission = await Notification.requestPermission(); + if (permission === 'granted') { + console.log('Notification permission granted'); + } + } catch (error) { + console.error('Error requesting notification permission:', error); + } +}; \ No newline at end of file diff --git a/apps/mobile/src/services/notifications/webPushNotification.ts b/apps/mobile/src/services/notifications/webPushNotification.ts new file mode 100644 index 000000000..692873aed --- /dev/null +++ b/apps/mobile/src/services/notifications/webPushNotification.ts @@ -0,0 +1,26 @@ +export interface WebPushSubscription { + endpoint: string; + keys: { + p256dh: string; + auth: string; + }; +} + +export async function subscribeToWebPush(): Promise { + if (!('serviceWorker' in navigator) || !('PushManager' in window)) { + return null; + } + + try { + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: process.env.EXPO_PUBLIC_VAPID_KEY + }); + + return subscription.toJSON() as WebPushSubscription; + } catch (error) { + console.error('Failed to subscribe to web push:', error); + return null; + } +} \ No newline at end of file diff --git a/apps/mobile/src/utils/notifications.ts b/apps/mobile/src/utils/notifications.ts index 42fb38866..509e11fd1 100644 --- a/apps/mobile/src/utils/notifications.ts +++ b/apps/mobile/src/utils/notifications.ts @@ -14,3 +14,14 @@ export const sendNotificationForEvent = async ( throw error; } }; + +export async function requestNotificationPermission() { + if ('Notification' in window) { + const permission = await Notification.requestPermission(); + if (permission === 'granted') { + // Subscribe to push notifications here + const registration = await navigator.serviceWorker.ready; + // You'll need to implement your push subscription logic here + } + } +} diff --git a/apps/pwa/.expo/README.md b/apps/pwa/.expo/README.md new file mode 100644 index 000000000..f7eb5fe7e --- /dev/null +++ b/apps/pwa/.expo/README.md @@ -0,0 +1,8 @@ +> Why do I have a folder named ".expo" in my project? +The ".expo" folder is created when an Expo project is started using "expo start" command. +> What do the files contain? +- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds. +- "settings.json": contains the server configuration that is used to serve the application manifest. +> Should I commit the ".expo" folder? +No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine. +Upon project creation, the ".expo" folder is already added to your ".gitignore" file. diff --git a/apps/pwa/.expo/devices.json b/apps/pwa/.expo/devices.json new file mode 100644 index 000000000..5efff6c8c --- /dev/null +++ b/apps/pwa/.expo/devices.json @@ -0,0 +1,3 @@ +{ + "devices": [] +} diff --git a/apps/pwa/package.json b/apps/pwa/package.json index 2da2e66d2..114be0dee 100644 --- a/apps/pwa/package.json +++ b/apps/pwa/package.json @@ -15,6 +15,7 @@ "ts:check": "tsc --noEmit" }, "dependencies": { + "@argent/x-sessions": "^6.7.4", "@avnu/avnu-sdk": "^2.0.0", "@chakra-ui/next-js": "^2.2.0", "@chakra-ui/react": "^2.8.2", @@ -45,13 +46,11 @@ "react-transition-group": "^4.4.5", "react-use-websocket": "^4.8.1", "starknet": "6.9.0", + "starknetkit-next": "npm:starknetkit@2.3.0", "viem": "~2.21.2", "wagmi": "^2.12.8", "zod": "^3.23.8", - "zustand": "^4.5.2", - "starknetkit-next": "npm:starknetkit@2.3.0", - "@argent/x-sessions": "^6.7.4" - + "zustand": "^4.5.2" }, "devDependencies": { "@types/node": "^20", @@ -62,6 +61,7 @@ "eslint": "^8.57.0", "eslint-config-next": "^14.2.4", "framer-motion": "^11.2.4", + "pino-pretty": "^11.3.0", "postcss": "^8.4.38", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/apps/pwa/public/service-worker.js b/apps/pwa/public/service-worker.js new file mode 100644 index 000000000..793b7f07f --- /dev/null +++ b/apps/pwa/public/service-worker.js @@ -0,0 +1,19 @@ +self.addEventListener('push', event => { + const options = { + body: event.data?.text() || 'New notification', + icon: '/notification-icon.png', + badge: '/badge-icon.png', + vibrate: [200, 100, 200] + }; + + event.waitUntil( + self.registration.showNotification('AFK Community', options) + ); +}); + +self.addEventListener('notificationclick', event => { + event.notification.close(); + event.waitUntil( + clients.openWindow('/') + ); +}); \ No newline at end of file diff --git a/apps/pwa/src/app/layout.tsx b/apps/pwa/src/app/layout.tsx index bd83f564f..b4c7f1dad 100644 --- a/apps/pwa/src/app/layout.tsx +++ b/apps/pwa/src/app/layout.tsx @@ -4,6 +4,8 @@ import '@rainbow-me/rainbowkit/styles.css'; import type {Metadata} from 'next'; import Providers from './providers'; +import {NotificationProvider} from './providers/NotificationProvider'; + export const metadata: Metadata = { title: 'afk community portal', description: 'afk community portal', @@ -13,7 +15,11 @@ export default function RootLayout({children}: {children: React.ReactNode}) { return ( - {children} + + + {children} + + ); diff --git a/apps/pwa/src/app/providers.tsx b/apps/pwa/src/app/providers.tsx index c3c82d8e4..29fc46a75 100644 --- a/apps/pwa/src/app/providers.tsx +++ b/apps/pwa/src/app/providers.tsx @@ -5,73 +5,26 @@ import '@rainbow-me/rainbowkit/styles.css'; import {ChakraProvider} from '@chakra-ui/react'; import {getDefaultConfig, RainbowKitProvider} from '@rainbow-me/rainbowkit'; import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; -import {Chain} from 'viem'; -import {createConfig, http} from 'wagmi'; import {WagmiProvider} from 'wagmi'; -import {mainnet, sepolia} from 'wagmi/chains'; import StarknetProvider from '@/context/StarknetProvider'; - -// import {TanstackProvider} from 'afk_nostr_sdk'; -// import {NostrProvider} from 'afk_nostr_sdk'; - -const kakarotEvm: Chain = { - id: 1802203764, - name: 'Kakarot Sepolia', - // network: "Scroll Sepolia Testnet", - // iconUrl: '/assets/scroll.svg', - // iconBackground: '#fff', - nativeCurrency: { - decimals: 18, - name: 'Ethereum', - symbol: 'ETH ', - }, - rpcUrls: { - public: {http: ['https://sepolia-rpc.kakarot.org']}, - default: {http: ['https://sepolia-rpc.kakarot.org']}, - }, - blockExplorers: { - default: {name: 'Explorer', url: 'https://sepolia.kakarotscan.org/'}, - etherscan: {name: 'Explorer', url: 'https://sepolia.kakarotscan.org/'}, - }, - // testnet: true, -}; - -export const config = createConfig({ - chains: [mainnet, sepolia, kakarotEvm], - transports: { - [mainnet.id]: http(), - [sepolia.id]: http(), - [kakarotEvm.id]: http(), - }, -}); - -const configRainbow = getDefaultConfig({ - appName: 'My RainbowKit App', - projectId: 'YOUR_PROJECT_ID', - chains: [mainnet, sepolia], - transports: { - [mainnet.id]: http('https://eth-mainnet.g.alchemy.com/v2/...'), - [sepolia.id]: http('https://eth-sepolia.g.alchemy.com/v2/...'), - }, -}); +import {NotificationProvider} from './providers/NotificationProvider'; +import {config} from './wagmiConfig'; // We'll create this file const queryClient = new QueryClient(); export default function Providers({children}: {children: React.ReactNode}) { return ( - <> - - - - - {children} - - - - - + + + + + + {children} + + + + + ); } diff --git a/apps/pwa/src/app/providers/NotificationProvider.tsx b/apps/pwa/src/app/providers/NotificationProvider.tsx new file mode 100644 index 000000000..695263e0f --- /dev/null +++ b/apps/pwa/src/app/providers/NotificationProvider.tsx @@ -0,0 +1,31 @@ +'use client'; +import {createContext, useContext, useEffect} from 'react'; +import {requestNotificationPermission, setupServiceWorker} from '../../services/notifications'; + +const NotificationContext = createContext({}); + +export function NotificationProvider({children}: {children: React.ReactNode}) { + useEffect(() => { + const initNotifications = async () => { + const permission = await requestNotificationPermission(); + if (permission) { + const registration = await setupServiceWorker(); + if (registration) { + try { + const subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: process.env.NEXT_PUBLIC_VAPID_KEY + }); + console.log('Push notification subscription:', subscription); + } catch (err) { + console.error('Failed to subscribe to push notifications:', err); + } + } + } + }; + + initNotifications(); + }, []); + + return {children}; +} \ No newline at end of file diff --git a/apps/pwa/src/app/wagmiConfig.ts b/apps/pwa/src/app/wagmiConfig.ts new file mode 100644 index 000000000..66b74a89f --- /dev/null +++ b/apps/pwa/src/app/wagmiConfig.ts @@ -0,0 +1,31 @@ +import {getDefaultConfig} from '@rainbow-me/rainbowkit'; +import {mainnet, sepolia} from 'wagmi/chains'; +import {http} from 'wagmi'; + +const kakarotEvm = { + id: 1802203764, + name: 'Kakarot Sepolia', + nativeCurrency: { + decimals: 18, + name: 'Ethereum', + symbol: 'ETH', + }, + rpcUrls: { + public: {http: ['https://sepolia-rpc.kakarot.org']}, + default: {http: ['https://sepolia-rpc.kakarot.org']}, + }, + blockExplorers: { + default: {name: 'Explorer', url: 'https://sepolia.kakarotscan.org/'}, + }, +}; + +export const config = getDefaultConfig({ + appName: 'AFK Community', + projectId: process.env.NEXT_PUBLIC_WC_ID || '', + chains: [mainnet, sepolia, kakarotEvm], + transports: { + [mainnet.id]: http(), + [sepolia.id]: http(), + [kakarotEvm.id]: http(), + }, +}); \ No newline at end of file diff --git a/apps/pwa/src/services/notifications.ts b/apps/pwa/src/services/notifications.ts new file mode 100644 index 000000000..ab9d2ab3c --- /dev/null +++ b/apps/pwa/src/services/notifications.ts @@ -0,0 +1,32 @@ + +export async function requestNotificationPermission(): Promise { + if (!('Notification' in window)) { + console.log('This browser does not support notifications'); + return false; + } + + const permission = await Notification.requestPermission(); + return permission === 'granted'; +} + +export async function showNotification(title: string, options?: NotificationOptions) { + if (!('Notification' in window)) { + return; + } + + if (Notification.permission === 'granted') { + return new Notification(title, options); + } +} + +export async function setupServiceWorker() { + if ('serviceWorker' in navigator) { + try { + const registration = await navigator.serviceWorker.register('/service-worker.js'); + console.log('ServiceWorker registration successful'); + return registration; + } catch (error) { + console.error('ServiceWorker registration failed:', error); + } + } +} \ No newline at end of file diff --git a/apps/pwa/tsconfig.json b/apps/pwa/tsconfig.json index ceede022d..dbe0a8a40 100644 --- a/apps/pwa/tsconfig.json +++ b/apps/pwa/tsconfig.json @@ -29,8 +29,12 @@ "@/*": [ "./src/*" ], - "pixel_ui": ["../../packages/pixel_ui/src"], - "common": ["../../packages/common/src"] + "pixel_ui": [ + "../../packages/pixel_ui/src" + ], + "common": [ + "../../packages/common/src" + ] }, "noEmit": true }, @@ -40,8 +44,10 @@ "**/*.tsx", "**/*.jsx", "**/*.js", - ".next/types/**/*.ts" ], + ".next/types/**/*.ts" + ], "exclude": [ "node_modules" - ] + ], + "extends": "expo/tsconfig.base" } From 469ca8615974855ecbfd46d02ea628c574c2615d Mon Sep 17 00:00:00 2001 From: Nathan_akin <85641756+akintewe@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:12:02 +0100 Subject: [PATCH 5/5] made changes --- apps/pwa/src/app/components/MenuNav.tsx | 69 ++++++++++++------- .../src/app/components/NavigationLinks.tsx | 17 ++--- apps/pwa/src/app/page.tsx | 4 +- apps/pwa/src/app/settings/page.tsx | 18 +++++ apps/pwa/src/components/Navigation.tsx | 6 ++ .../src/components/NotificationsSettings.tsx | 59 ++++++++++++++++ 6 files changed, 136 insertions(+), 37 deletions(-) create mode 100644 apps/pwa/src/app/settings/page.tsx create mode 100644 apps/pwa/src/components/Navigation.tsx create mode 100644 apps/pwa/src/components/NotificationsSettings.tsx diff --git a/apps/pwa/src/app/components/MenuNav.tsx b/apps/pwa/src/app/components/MenuNav.tsx index 2004c05df..6e3943b9b 100644 --- a/apps/pwa/src/app/components/MenuNav.tsx +++ b/apps/pwa/src/app/components/MenuNav.tsx @@ -1,34 +1,57 @@ import {Button, Menu, MenuButton, MenuItem, MenuList} from '@chakra-ui/react'; - +import Link from 'next/link'; +import {useRouter} from 'next/navigation'; import {CustomConnectButtonWallet} from './button/CustomConnectButtonWallet'; interface IMenuParent { children?: React.ReactNode; } + const MenuNav: React.FC = () => { + const router = useRouter(); + + const handleSettingsClick = () => { + router.push('/settings'); + }; + return ( - - {({isOpen}) => ( - <> - } - /> - } - > - {isOpen ? 'Close' : 'Profile'} - - - - - - - - )} - +
+ + + + {({isOpen}) => ( + <> + + {isOpen ? 'Close' : 'Profile'} + + + + + + + + )} + +
); }; diff --git a/apps/pwa/src/app/components/NavigationLinks.tsx b/apps/pwa/src/app/components/NavigationLinks.tsx index dc1a94bb9..9f7ddf3d8 100644 --- a/apps/pwa/src/app/components/NavigationLinks.tsx +++ b/apps/pwa/src/app/components/NavigationLinks.tsx @@ -1,20 +1,15 @@ 'use client'; +import Link from 'next/link'; + export function NavigationLinks() { return (
    - {/*
  • - Features -
  • - Pixel -
  • */} - {/*
  • - -
  • */} - {/* */} + + Settings + +
); } diff --git a/apps/pwa/src/app/page.tsx b/apps/pwa/src/app/page.tsx index d96be5646..6864791f9 100644 --- a/apps/pwa/src/app/page.tsx +++ b/apps/pwa/src/app/page.tsx @@ -1,6 +1,5 @@ 'use client'; import {AppRender} from 'pixel_ui'; - import {Navbar} from './components/Navbar'; export default function App() { @@ -12,9 +11,8 @@ export default function App() { artPeaceAddress={process.env.NEXT_PUBLIC_CANVAS_STARKNET_CONTRACT_ADDRESS} nftCanvasAddress={process.env.NEXT_PUBLIC_CANVAS_NFT_CONTRACT_ADDRESS} usernameAddress={process.env.NEXT_PUBLIC_USERNAME_STORE_CONTRACT_ADDRESS} - > + /> )} - {/*