Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: push notifications with one signal #6089

Draft
wants to merge 2 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@
"dompurify": "3.1.4",
"dotenv": "16.4.5",
"ecdsa-sig-formatter": "1.0.11",
"firebase": "11.2.0",
"formik": "2.4.5",
"framer-motion": "11.9.0",
"jotai": "2.2.1",
Expand Down
621 changes: 621 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion scripts/generate-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const manifest = {
manifest_version: 3,
author: 'Leather Wallet, LLC',
description: 'Leather Bitcoin Wallet - Your Bitcoin Wallet for DeFi, NFTs, Ordinals, and dApps',
permissions: ['contextMenus', 'storage', 'unlimitedStorage'],
permissions: ['contextMenus', 'storage', 'unlimitedStorage', 'notifications', 'gcm'],
commands: {
_execute_browser_action: {
suggested_key: {
Expand Down
3 changes: 2 additions & 1 deletion src/app/features/container/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useAppDispatch, useHasStateRehydrated } from '@app/store';
import { stxChainSlice } from '@app/store/chains/stx-chain.slice';

import { useRestoreFormState } from '../popup-send-form-restoration/use-restore-form-state';
import { useInitPushNotifications } from '../push-notifications/push-notifications';

export function Container() {
const { pathname: locationPathname } = useLocation();
Expand All @@ -33,9 +34,9 @@ export function Container() {
useOnSignOut(() => closeWindow());
useRestoreFormState();
useInitalizeAnalytics();
useInitPushNotifications();
useHandleQueuedBackgroundAnalytics();
useOnChangeAccount(index => dispatch(stxChainSlice.actions.switchAccount(index)));

useEffect(() => void analytics.page('view', `${pathname}`), [pathname]);

if (!hasStateRehydrated) return <LoadingSpinner />;
Expand Down
71 changes: 71 additions & 0 deletions src/app/features/push-notifications/push-notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useMemo } from 'react';

import type { HDKey } from '@scure/bip32';
import type { P2Ret } from '@scure/btc-signer/payment';
import axios from 'axios';

Check failure on line 5 in src/app/features/push-notifications/push-notifications.ts

View workflow job for this annotation

GitHub Actions / typecheck

'axios' is declared but its value is never read.

import {
type SupportedPaymentType,
deriveAddressIndexZeroFromAccount,
getNativeSegwitPaymentFromAddressIndex,
getTaprootPaymentFromAddressIndex,
isSupportedMessageSigningPaymentType,
} from '@leather.io/bitcoin';
import type { BitcoinNetworkModes } from '@leather.io/models';
import { createNullArrayOfLength, isDefined } from '@leather.io/utils';

import { useOnMount } from '@app/common/hooks/use-on-mount';
import { useGenerateNativeSegwitAccount } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useGenerateTaprootAccount } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useStacksChain } from '@app/store/chains/stx-chain.selectors';

const paymentFnMap: Record<
SupportedPaymentType,
(keychain: HDKey, network: BitcoinNetworkModes) => P2Ret
> = {
p2tr: getTaprootPaymentFromAddressIndex,
p2wpkh: getNativeSegwitPaymentFromAddressIndex,
};

isSupportedMessageSigningPaymentType;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol ignore this


export function useInitPushNotifications() {
const { highestAccountIndex } = useStacksChain().default;
const createNativeSegwitAccount = useGenerateNativeSegwitAccount();
const createTaprootAccount = useGenerateTaprootAccount();

const addresses = useMemo(

Check failure on line 37 in src/app/features/push-notifications/push-notifications.ts

View workflow job for this annotation

GitHub Actions / typecheck

'addresses' is declared but its value is never read.
() =>
createNullArrayOfLength(highestAccountIndex).flatMap((_, index) =>
[createNativeSegwitAccount(index), createTaprootAccount(index)]
.filter(isDefined)
.map(account => {
// console.log(account);
const addressIndexKeychain = deriveAddressIndexZeroFromAccount(account.keychain);
if (account.type !== 'p2tr' && account.type !== 'p2wpkh') return undefined;
const payment = paymentFnMap[account.type](addressIndexKeychain, 'mainnet');
return payment.address;
})
),
[createNativeSegwitAccount, createTaprootAccount, highestAccountIndex]
);

useOnMount(async () => {
// console.log('addresses', addresses);

const { fcmRegistrationToken } = await chrome.storage.local.get('fcmRegistrationToken');

Check failure on line 56 in src/app/features/push-notifications/push-notifications.ts

View workflow job for this annotation

GitHub Actions / typecheck

'fcmRegistrationToken' is declared but its value is never read.

// const resp = await axios.post(
// 'https://leather-api-gateway-staging.wallet-6d1.workers.dev/v1/notifications/register',
// {
// addresses,
// network: 'mainnet',
// chain: 'bitcoin',
// notificationToken: fcmRegistrationToken,
// }
// );

// eslint-disable-next-line no-console
// console.log({ resp });
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const selectCurrentNetworkNativeSegwitAccountBuilder = createSelector(
nativeSegwitKeychains[bitcoinNetworkToNetworkMode(network.chain.bitcoin.bitcoinNetwork)]
);

function useNativeSegwitAccountBuilder() {
export function useGenerateNativeSegwitAccount() {
return useSelector(selectCurrentNetworkNativeSegwitAccountBuilder);
}

Expand All @@ -72,7 +72,7 @@ export function useNativeSegwitNetworkSigners() {
}

export function useNativeSegwitSigner(accountIndex: number) {
const account = useNativeSegwitAccountBuilder()(accountIndex);
const account = useGenerateNativeSegwitAccount()(accountIndex);
const extendedPublicKeyVersions = useBitcoinExtendedPublicKeyVersions();

return useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ const selectCurrentNetworkTaprootAccountBuilder = createSelector(
selectCurrentNetwork,
(taprootKeychains, network) => taprootKeychains[network.chain.bitcoin.mode]
);

export function useGenerateTaprootAccount() {
const generateTaprootAccount = useSelector(selectCurrentNetworkTaprootAccountBuilder);
return useMemo(() => generateTaprootAccount, [generateTaprootAccount]);
}

const selectCurrentTaprootAccount = createSelector(
selectCurrentNetworkTaprootAccountBuilder,
selectCurrentAccountIndex,
Expand Down
6 changes: 6 additions & 0 deletions src/app/store/chains/stx-chain.selectors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { useSelector } from 'react-redux';

import { RootState } from '@app/store';

export const selectStacksChain = (state: RootState) => state.chains.stx;

export function useStacksChain() {
return useSelector(selectStacksChain);
}
23 changes: 23 additions & 0 deletions src/background/background.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//
// This file is the entrypoint to the extension's background script
// https://developer.chrome.com/docs/extensions/mv3/architecture-overview/#background_script
import { onBackgroundMessage } from 'firebase/messaging/sw';

Check failure on line 4 in src/background/background.ts

View workflow job for this annotation

GitHub Actions / typecheck

'onBackgroundMessage' is declared but its value is never read.

import { logger } from '@shared/logger';
import { CONTENT_SCRIPT_PORT, type LegacyMessageFromContentScript } from '@shared/message-types';
import { WalletRequests } from '@shared/rpc/rpc-methods';
Expand All @@ -13,10 +15,31 @@
isLegacyMessage,
} from './messaging/legacy/legacy-external-message-handler';
import { rpcMessageHandler } from './messaging/rpc-message-handler';
import { initializeFirebaseCloudMessaging } from './register-firebase';

initContextMenuActions();
warnUsersAboutDevToolsDangers();

const { onPushNotification } = initializeFirebaseCloudMessaging();

onPushNotification(payload => {
console.log('payload in');

Check failure on line 26 in src/background/background.ts

View workflow job for this annotation

GitHub Actions / lint-eslint

Unexpected console statement
const notification = payload.notification;
if (!notification) return;
console.log('firing notification');

Check failure on line 29 in src/background/background.ts

View workflow job for this annotation

GitHub Actions / lint-eslint

Unexpected console statement
chrome.notifications.create(
{
title: notification.title ?? 'default notification',
message: notification.body ?? 'default body',
iconUrl: notification.icon ?? 'https://placekittens.com/200/300',
type: 'basic',
},
notificationId => {
console.log('notificationId', notificationId);

Check failure on line 38 in src/background/background.ts

View workflow job for this annotation

GitHub Actions / lint-eslint

Unexpected console statement
}
);
});

chrome.runtime.onInstalled.addListener(async details => {
if (details.reason === 'install' && process.env.WALLET_ENVIRONMENT !== 'testing') {
await chrome.tabs.create({
Expand Down
47 changes: 47 additions & 0 deletions src/background/register-firebase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { initializeApp } from 'firebase/app';
import { type MessagePayload, type Messaging, getToken } from 'firebase/messaging';
import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw';

import { isString } from '@leather.io/utils';

import { logger } from '@shared/logger';

const firebaseConfig = {
apiKey: 'AIzaSyCNWvmvdt_qqObnEUdeDPNhsRKXeLVrsZk',
authDomain: 'leather-b5081.firebaseapp.com',
projectId: 'leather-b5081',
storageBucket: 'leather-b5081.firebasestorage.app',
messagingSenderId: '915379517791',
appId: '1:915379517791:web:c84472be91fd5e8c789eea',
};

async function getOrCreateFirebaseToken(messaging: Messaging) {
const { fcmRegistrationToken } = await chrome.storage.local.get('fcmRegistrationToken');
if (!isString(fcmRegistrationToken)) throw new Error('Invalid fcmRegistrationToken');
if (!fcmRegistrationToken) {
const fcmRegistrationToken = await getToken(messaging, {
serviceWorkerRegistration: (self as any).registration,
});
chrome.storage.local.set({ fcmRegistrationToken });
return fcmRegistrationToken;
}
return fcmRegistrationToken;
}

export function initializeFirebaseCloudMessaging() {
const app = initializeApp(firebaseConfig);

chrome.runtime.onStartup.addListener(async () => {
const messaging = getMessaging(app);
await getOrCreateFirebaseToken(messaging);
});

function onPushNotification(callback: (payload: MessagePayload) => void) {
onBackgroundMessage(getMessaging(app), payload => {
logger.info('Push notification', payload);
callback(payload);
});
}

return { app, onPushNotification };
}
Loading