diff --git a/packages/commonwealth/client/scripts/App.tsx b/packages/commonwealth/client/scripts/App.tsx index 8c900a80a34..bf04cf10930 100644 --- a/packages/commonwealth/client/scripts/App.tsx +++ b/packages/commonwealth/client/scripts/App.tsx @@ -11,7 +11,8 @@ import { ToastContainer } from 'react-toastify'; import { queryClient } from 'state/api/config'; import { DefaultPrivyProvider } from 'views/components/DefaultPrivyProvider/DefaultPrivyProvider'; import { DisableMavaOnMobile } from 'views/components/DisableMavaOnMobile'; -import ForceMobileAuth from 'views/components/ForceMobileAuth'; +import { PrivyMobileAuthStatusProvider } from 'views/components/PrivyMobile/PrivyMobileAuthStatusProvider'; +import { PrivyMobileAuthenticator } from 'views/components/PrivyMobile/PrivyMobileAuthenticator'; import { ReactNativeBridgeUser } from 'views/components/ReactNativeBridge'; import { ReactNativeLogForwarder } from 'views/components/ReactNativeBridge/ReactNativeLogForwarder'; import { ReactNativeScrollToTopListener } from 'views/components/ReactNativeBridge/ReactNativeScrollToTopListener'; @@ -31,22 +32,28 @@ const App = () => { + {/*@ts-expect-error StrictNullChecks*/} {isLoading ? ( ) : ( - - - - - - - - - - + <> + + + + {/**/} + + + + + + {/**/} + + + + )} {import.meta.env.DEV && } diff --git a/packages/commonwealth/client/scripts/controllers/app/webWallets/privy_ethereum_web_wallet.ts b/packages/commonwealth/client/scripts/controllers/app/webWallets/privy_ethereum_web_wallet.ts index e1814a1b861..2a6cf848cf8 100644 --- a/packages/commonwealth/client/scripts/controllers/app/webWallets/privy_ethereum_web_wallet.ts +++ b/packages/commonwealth/client/scripts/controllers/app/webWallets/privy_ethereum_web_wallet.ts @@ -98,7 +98,6 @@ export class PrivyEthereumWebWalletController implements IWebWallet { public async enable(forceChainId?: string) { // TODO: use https://docs.metamask.io/guide/rpc-api.html#other-rpc-methods to switch active // chain according to currently active node, if one exists - console.log('Attempting to enable Metamask'); this._enabling = true; try { // default to ETH @@ -188,6 +187,7 @@ export class PrivyEthereumWebWalletController implements IWebWallet { throw switchError; } } + // fetch active accounts this._accounts = ( await this._web3.givenProvider.request({ diff --git a/packages/commonwealth/client/scripts/hooks/mobile/useMobileRPCEventReceiver.ts b/packages/commonwealth/client/scripts/hooks/mobile/useMobileRPCEventReceiver.ts new file mode 100644 index 00000000000..ba94bd25355 --- /dev/null +++ b/packages/commonwealth/client/scripts/hooks/mobile/useMobileRPCEventReceiver.ts @@ -0,0 +1,80 @@ +import { useCallback } from 'react'; +import { useReactNativeWebView } from '../useReactNativeWebView'; + +type EvenSubscribeMessage = { + $id: string; + type: string; + variant: 'event-subscribe'; + eventName: string; +}; + +type EventUpdateMessage = { + $id: string; + type: string; + variant: 'event-update'; + data: EventData; +}; + +export function useMobileRPCEventReceiver(type: string) { + const reactNativeWebView = useReactNativeWebView(); + + return useCallback( + (eventName: string, listener: (update: EventData) => void) => { + const $id = '' + Math.random() * 100000; + + const subscription: EvenSubscribeMessage = { + $id, + type: type, + eventName, + variant: 'event-subscribe', + }; + + if (!reactNativeWebView) { + return; + } + + reactNativeWebView.postMessage(JSON.stringify(subscription)); + + function handler(message: MessageEvent) { + const eventUpdateMessage = toEventUpdateMessage( + type, + message.data, + ); + + if (eventUpdateMessage?.$id === $id) { + console.log('Got event update: ', eventUpdateMessage); + listener(eventUpdateMessage.data); + } + } + + window.addEventListener('message', handler); + }, + [reactNativeWebView, type], + ); +} + +function toEventUpdateMessage( + type: string, + data: any, +): EventUpdateMessage | null { + const obj = messageToObject(data); + + if (obj && obj.type === type && obj.variant === 'event-update') { + return obj; + } + + return null; +} + +function messageToObject(message: string | any): any | null { + if (message === 'string') { + try { + return JSON.parse(message); + } catch (e) { + // this might be just a string sent with sendMessage + return null; + } + } + + return typeof message === 'string' ? JSON.parse(message) : message; +} diff --git a/packages/commonwealth/client/scripts/hooks/mobile/useMobileRPCSender.ts b/packages/commonwealth/client/scripts/hooks/mobile/useMobileRPCSender.ts new file mode 100644 index 00000000000..d9c8eefbaeb --- /dev/null +++ b/packages/commonwealth/client/scripts/hooks/mobile/useMobileRPCSender.ts @@ -0,0 +1,105 @@ +import { useCallback } from 'react'; + +type ProtoError = { + message: string; +}; + +type ProtoRequestObject = { + $id: string; + type: string; + variant: 'request'; + data: Request; +}; + +type ProtoResponseObjectSuccess = { + $id: string; + type: string; + variant: 'response'; + data: Response; + error: null; +}; + +/** + * Wraps a response so that it includes the error OR data. + */ +type ProtoResponseObjectFailure = { + $id: string; + type: string; + variant: 'response'; + data: null; + error: ProtoError; +}; + +type ProtoResponseObject = + | ProtoResponseObjectSuccess + | ProtoResponseObjectFailure; + +type Opts = { + type: string; +}; + +export function useMobileRPCSender(opts: Opts) { + return useCallback( + async (request: Request) => { + return new Promise((resolve, reject) => { + const $id = '' + Math.random() * 100000; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function handler(message: MessageEvent) { + const protoResponse = toProtoResponse( + opts.type, + message.data, + ); + + if (protoResponse?.$id === $id) { + console.log('Got proto response: ', protoResponse); + + if (protoResponse.error) { + reject(protoResponse.error); + } else { + resolve(protoResponse.data); + } + } + } + + window.addEventListener('message', handler); + + const protoRequest: ProtoRequestObject = { + $id, + type: opts.type, + variant: 'request', + data: request, + }; + + window.ReactNativeWebView!.postMessage(JSON.stringify(protoRequest)); + }); + }, + [opts.type], + ); +} + +function toProtoResponse( + type: string, + data: any, +): ProtoResponseObject | null { + const obj = messageToObject(data); + + if (obj && obj.type === type && obj.variant === 'response') { + return obj; + } + + return null; +} + +function messageToObject(message: string | any): any | null { + if (message === 'string') { + try { + return JSON.parse(message); + } catch (e) { + // this might be just a string sent with sendMessage + return null; + } + } + + return typeof message === 'string' ? JSON.parse(message) : message; +} diff --git a/packages/commonwealth/client/scripts/hooks/useReactNativeWebView.ts b/packages/commonwealth/client/scripts/hooks/useReactNativeWebView.ts index 21d2cdb98a0..85c3b295085 100644 --- a/packages/commonwealth/client/scripts/hooks/useReactNativeWebView.ts +++ b/packages/commonwealth/client/scripts/hooks/useReactNativeWebView.ts @@ -72,14 +72,26 @@ export async function execWithinMobileApp< // eslint-disable-next-line @typescript-eslint/no-explicit-any function handler(message: MessageEvent) { + console.log('FIXME execWithinMobileApp handler received message!'); const dataObj = messageToObject(message.data); + // FIXME: it's possible messageToObject could be throwing an error? + + console.log( + 'FIXME execWithinMobileApp: got message: ', + JSON.stringify(dataObj, null, 2), + ); + if (dataObj.__requestID === __requestID) { latch.resolve(dataObj as Output); } } - addEventListener('message', handler); + console.log( + 'FIXME execWithinMobileApp adding event listener to listen for response message', + ); + + window.addEventListener('message', handler); window.ReactNativeWebView!.postMessage( JSON.stringify({ @@ -88,13 +100,17 @@ export async function execWithinMobileApp< }), ); + console.log('FIXME execWithinMobileApp waiting fr resolution '); + // the event listener we just registered will keep listening until the // latch is revolved and gets the response. - await latch.promise; + const output = await latch.promise; + + console.log('FIXME execWithinMobileApp RESOLVED!!! '); // now we have to remove the event listener before we return the latch and // clean up after ourselves. - removeEventListener('message', handler); + window.removeEventListener('message', handler); - return latch.promise; + return output; } diff --git a/packages/commonwealth/client/scripts/navigation/CommonDomainRoutes.tsx b/packages/commonwealth/client/scripts/navigation/CommonDomainRoutes.tsx index 80f54525bcc..336c156740b 100644 --- a/packages/commonwealth/client/scripts/navigation/CommonDomainRoutes.tsx +++ b/packages/commonwealth/client/scripts/navigation/CommonDomainRoutes.tsx @@ -1,6 +1,7 @@ import { Navigate } from 'navigation/helpers'; import React, { lazy } from 'react'; import { Route } from 'react-router-dom'; +import { DebugMobile } from 'views/components/DebugMobile/DebugMobile'; import { SignIn } from 'views/components/SignIn/SignIn'; import { withLayout } from 'views/Layout'; import { MobileSignIn } from 'views/modals/MobileSignIn/MobileSignIn'; @@ -145,6 +146,12 @@ const newProposalViewPage = lazy( ); const CommonDomainRoutes = () => [ + } + />, + { // eslint-disable-next-line @typescript-eslint/no-explicit-any const response = await execWithinMobileApp({ @@ -44,6 +50,9 @@ export class MobileNotifications { }; } + /** + * @deprecated + */ public static async requestPermissionsAsync(): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const response = await execWithinMobileApp({ diff --git a/packages/commonwealth/client/scripts/views/components/DebugMobile/DebugMobile.tsx b/packages/commonwealth/client/scripts/views/components/DebugMobile/DebugMobile.tsx new file mode 100644 index 00000000000..e10b27c3766 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/DebugMobile/DebugMobile.tsx @@ -0,0 +1,105 @@ +import React, { memo, useState } from 'react'; +import { DebugPostMessage } from 'views/components/PrivyMobile/DebugPostMessage'; +import { usePrivyEthereumWalletRequest } from 'views/components/PrivyMobile/usePrivyEthereumWalletRequest'; +import usePrivyMobileAuthStatusStore from 'views/components/PrivyMobile/usePrivyMobileAuthStatusStore'; +import { usePrivyMobileLogout } from 'views/components/PrivyMobile/usePrivyMobileLogout'; +import { usePrivyMobileSignMessage } from 'views/components/PrivyMobile/usePrivyMobileSignMessage'; +import { useNotificationsRequestPermissionsAsyncReceiver } from '../PrivyMobile/useNotificationsRequestPermissionsAsyncReceiver'; + +/** + * component to help debug mobile usage. + */ +export const DebugMobile = memo(function DebugMobile() { + const { status: privyMobileAuthStatus } = usePrivyMobileAuthStatusStore(); + + const [signature, setSignature] = useState(); + + const [accounts, setAccounts] = useState(); + const [notificationPermissions, setNotificationPermissions] = useState< + string | undefined + >(); + + const signMessage = usePrivyMobileSignMessage(); + const logout = usePrivyMobileLogout(); + const ethereumWalletRequest = usePrivyEthereumWalletRequest(); + + const requestNotificationsPermissions = + useNotificationsRequestPermissionsAsyncReceiver(); + + const handleSignMessage = () => { + async function doAsync() { + const result = await signMessage('hello'); + setSignature(result); + } + + doAsync().catch(console.error); + }; + + const handleLogout = () => { + async function doAsync() { + await logout({}); + } + + doAsync().catch(console.error); + }; + + const handleEthereumWalletRequest = () => { + async function doAsync() { + const tmp = await ethereumWalletRequest({ + method: 'eth_requestAccounts', + }); + setAccounts(tmp); + } + + doAsync().catch(console.error); + }; + + const handleRequestNotificationsPermissions = () => { + async function doAsync() { + const { status } = await requestNotificationsPermissions({}); + setNotificationPermissions(status); + } + + doAsync().catch(console.error); + }; + + return ( + +
+
+ privyMobileAuthStatus: +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + {signature &&
signature: {signature}
} + + {accounts &&
accounts: {JSON.stringify(accounts, null, 2)}
} + + {notificationPermissions && ( +
notificationPermissions: {notificationPermissions}
+ )} + +
{JSON.stringify(privyMobileAuthStatus, null, 2)}
+
+
+ ); +}); diff --git a/packages/commonwealth/client/scripts/views/components/DefaultPrivyProvider/WaitForPrivy.tsx b/packages/commonwealth/client/scripts/views/components/DefaultPrivyProvider/WaitForPrivy.tsx index 581bb8cee02..83e428b43e8 100644 --- a/packages/commonwealth/client/scripts/views/components/DefaultPrivyProvider/WaitForPrivy.tsx +++ b/packages/commonwealth/client/scripts/views/components/DefaultPrivyProvider/WaitForPrivy.tsx @@ -1,6 +1,6 @@ import { usePrivy } from '@privy-io/react-auth'; import React, { memo } from 'react'; -import { PageLoading } from 'views/pages/loading'; +import { LoadingIndicator } from 'views/components/LoadingIndicator/LoadingIndicator'; import './WaitForPrivy.scss'; type WaitForPrivyProps = { @@ -17,7 +17,7 @@ export const WaitForPrivy = memo(function WaitForPrivy( if (!ready) { return (
- ; + ;
); } diff --git a/packages/commonwealth/client/scripts/views/pages/loading.scss b/packages/commonwealth/client/scripts/views/components/LoadingIndicator/LoadingIndicator.scss similarity index 92% rename from packages/commonwealth/client/scripts/views/pages/loading.scss rename to packages/commonwealth/client/scripts/views/components/LoadingIndicator/LoadingIndicator.scss index 6c87e5bcc2d..2c3ba37aff1 100644 --- a/packages/commonwealth/client/scripts/views/pages/loading.scss +++ b/packages/commonwealth/client/scripts/views/components/LoadingIndicator/LoadingIndicator.scss @@ -1,4 +1,4 @@ -.LoadingPage { +.LoadingIndicator { align-items: center; display: flex; flex: 1; diff --git a/packages/commonwealth/client/scripts/views/components/LoadingIndicator/LoadingIndicator.tsx b/packages/commonwealth/client/scripts/views/components/LoadingIndicator/LoadingIndicator.tsx new file mode 100644 index 00000000000..5bbee446020 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/LoadingIndicator/LoadingIndicator.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CWText } from '../component_kit/cw_text'; +import CWCircleMultiplySpinner from '../component_kit/new_designs/CWCircleMultiplySpinner'; +import './LoadingIndicator.scss'; + +type LoadingIndicatorProps = { + message?: string; +}; + +export const LoadingIndicator = (props: LoadingIndicatorProps) => { + const { message } = props; + + return ( +
+
+ + {message} +
+
+ ); +}; diff --git a/packages/commonwealth/client/scripts/views/components/LoadingIndicator/index.tsx b/packages/commonwealth/client/scripts/views/components/LoadingIndicator/index.tsx new file mode 100644 index 00000000000..266c417377e --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/LoadingIndicator/index.tsx @@ -0,0 +1 @@ +export * from './LoadingIndicator'; diff --git a/packages/commonwealth/client/scripts/views/components/LoadingIndicatorScreen/LoadingIndicatorScreen.scss b/packages/commonwealth/client/scripts/views/components/LoadingIndicatorScreen/LoadingIndicatorScreen.scss new file mode 100644 index 00000000000..44738d2df1e --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/LoadingIndicatorScreen/LoadingIndicatorScreen.scss @@ -0,0 +1,6 @@ +.LoadingIndicatorScreen { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} diff --git a/packages/commonwealth/client/scripts/views/components/LoadingIndicatorScreen/LoadingIndicatorScreen.tsx b/packages/commonwealth/client/scripts/views/components/LoadingIndicatorScreen/LoadingIndicatorScreen.tsx new file mode 100644 index 00000000000..a91185752b2 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/LoadingIndicatorScreen/LoadingIndicatorScreen.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { LoadingIndicator } from 'views/components/LoadingIndicator'; +import './LoadingIndicatorScreen.scss'; + +/** + * A full screen loading indicator. + */ +export const LoadingIndicatorScreen = () => { + return ( +
+ +
+ ); +}; diff --git a/packages/commonwealth/client/scripts/views/components/LoadingIndicatorScreen/index.tsx b/packages/commonwealth/client/scripts/views/components/LoadingIndicatorScreen/index.tsx new file mode 100644 index 00000000000..6abba5ece93 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/LoadingIndicatorScreen/index.tsx @@ -0,0 +1 @@ +export * from './LoadingIndicatorScreen'; diff --git a/packages/commonwealth/client/scripts/views/components/Privy/helpers.ts b/packages/commonwealth/client/scripts/views/components/Privy/helpers.ts index 6249036e50e..018e4389299 100644 --- a/packages/commonwealth/client/scripts/views/components/Privy/helpers.ts +++ b/packages/commonwealth/client/scripts/views/components/Privy/helpers.ts @@ -1,14 +1,26 @@ -import { - OAuthProvider, - PrivySignInSSOProvider, -} from 'views/components/Privy/types'; +import { WalletSsoSource } from '@hicommonwealth/shared'; +import { PrivySignInSSOProvider } from 'views/components/Privy/types'; export function toSignInProvider( - provider: OAuthProvider, + provider: WalletSsoSource, ): PrivySignInSSOProvider { switch (provider) { case 'google': return 'google_oauth'; + case 'github': + return 'github_oauth'; + case 'discord': + return 'discord_oauth'; + case 'twitter': + return 'twitter_oauth'; + case 'apple': + return 'apple_oauth'; + case 'email': + return 'email'; + case 'farcaster': + return 'farcaster'; + case 'SMS': + return 'phone'; default: throw new Error('Not supported: ' + provider); } diff --git a/packages/commonwealth/client/scripts/views/components/Privy/types.ts b/packages/commonwealth/client/scripts/views/components/Privy/types.ts index 3270ba64eb3..0fe1d9ab641 100644 --- a/packages/commonwealth/client/scripts/views/components/Privy/types.ts +++ b/packages/commonwealth/client/scripts/views/components/Privy/types.ts @@ -1,4 +1,12 @@ -export type PrivySignInSSOProvider = 'email' | 'phone' | 'google_oauth'; +export type PrivySignInSSOProvider = + | 'google_oauth' + | 'github_oauth' + | 'discord_oauth' + | 'apple_oauth' + | 'twitter_oauth' + | 'phone' + | 'farcaster' + | 'email'; export type OAuthProvider = | 'google' @@ -6,14 +14,3 @@ export type OAuthProvider = | 'discord' | 'twitter' | 'apple'; - -export type PrivyOAuthProvider = - | 'google' - | 'discord' - | 'twitter' - | 'github' - | 'spotify' - | 'instagram' - | 'tiktok' - | 'linkedin' - | 'apple'; diff --git a/packages/commonwealth/client/scripts/views/components/PrivyMobile/DebugPostMessage.tsx b/packages/commonwealth/client/scripts/views/components/PrivyMobile/DebugPostMessage.tsx new file mode 100644 index 00000000000..4f0fbe23019 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/PrivyMobile/DebugPostMessage.tsx @@ -0,0 +1,24 @@ +import React, { memo, useCallback, useEffect } from 'react'; + +type Props = { + children: React.ReactNode; +}; + +export const DebugPostMessage = memo(function DebugPostMessage(props: Props) { + const { children } = props; + const handler = useCallback((message: MessageEvent) => { + console.log('GOT POST MESSAGE' + JSON.stringify(message.data, null, 2)); + }, []); + + useEffect(() => { + console.log('Listening for all post messages'); + window.addEventListener('message', handler); + + return () => { + console.log('Removing post message listener'); + window.removeEventListener('message', handler); + }; + }, [handler]); + + return children; +}); diff --git a/packages/commonwealth/client/scripts/views/components/PrivyMobile/PrivyMobileAuthStatusProvider.ts b/packages/commonwealth/client/scripts/views/components/PrivyMobile/PrivyMobileAuthStatusProvider.ts new file mode 100644 index 00000000000..74d72cd5e38 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/PrivyMobile/PrivyMobileAuthStatusProvider.ts @@ -0,0 +1,58 @@ +import { useCallback, useEffect } from 'react'; +import { IPrivyAuthStatus } from 'views/components/PrivyMobile/types'; +import usePrivyMobileAuthStatusStore from 'views/components/PrivyMobile/usePrivyMobileAuthStatusStore'; +import { messageToObject } from '../ReactNativeBridge/utils'; + +type Props = { + children: React.ReactNode; +}; + +/** + * This keeps the privy auth state, from the mobile app, using mobile privy, + * available for use within the app. + * + * @deprecated TODO we don't need this now. + * + */ +export const PrivyMobileAuthStatusProvider = (props: Props) => { + const { children } = props; + const { setState } = usePrivyMobileAuthStatusStore(); + + const handleMessage = useCallback( + (message: MessageEvent) => { + const obj = messageToObject(message.data); + if (obj && typeof message.data === 'object') { + if (isPrivyAuthStatusMessage(obj)) { + console.log( + 'Privy auth status message received', + JSON.stringify(obj, null, 2), + ); + setState({ status: obj.data }); + } + } + }, + [setState], + ); + + useEffect(() => { + window.addEventListener('message', handleMessage); + + return () => { + window.removeEventListener('message', handleMessage); + }; + }, [handleMessage]); + + return children; +}; + +type PrivyAuthStatusMessage = { + type: 'privy.auth-status'; + data: IPrivyAuthStatus; +}; + +function isPrivyAuthStatusMessage( + data: object, +): data is PrivyAuthStatusMessage { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return !!data && (data as any).type === 'privy.auth-status'; +} diff --git a/packages/commonwealth/client/scripts/views/components/PrivyMobile/PrivyMobileAuthenticator.tsx b/packages/commonwealth/client/scripts/views/components/PrivyMobile/PrivyMobileAuthenticator.tsx new file mode 100644 index 00000000000..b7eec661750 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/PrivyMobile/PrivyMobileAuthenticator.tsx @@ -0,0 +1,136 @@ +import { ChainBase, WalletId } from '@hicommonwealth/shared'; +import { PrivyEthereumWebWalletController } from 'controllers/app/webWallets/privy_ethereum_web_wallet'; +import { getSessionFromWallet } from 'controllers/server/sessions'; +import React, { ReactNode, useCallback, useEffect } from 'react'; +import { useSignIn } from 'state/api/user'; +import useUserStore from 'state/ui/user'; +import { LoadingIndicatorScreen } from 'views/components/LoadingIndicatorScreen'; +import { toSignInProvider } from 'views/components/Privy/helpers'; +import { usePrivyEthereumWalletOn } from 'views/components/PrivyMobile/usePrivyEthereumWalletOn'; +import { usePrivyEthereumWalletRequest } from 'views/components/PrivyMobile/usePrivyEthereumWalletRequest'; +import { usePrivyMobileAuthStatus } from 'views/components/PrivyMobile/usePrivyMobileAuthStatus'; +import { usePrivyMobileLogout } from 'views/components/PrivyMobile/usePrivyMobileLogout'; +import { usePrivyMobileSignMessage } from 'views/components/PrivyMobile/usePrivyMobileSignMessage'; + +declare global { + interface Window { + PRIVY_MOBILE_ENABLED?: boolean; + } +} + +type Props = { + children: ReactNode; +}; + +/** + * Triggers authentication when privy mobile is enabled. + */ +export const PrivyMobileAuthenticator = (props: Props) => { + const { children } = props; + const getPrivyMobileAuthStatus = usePrivyMobileAuthStatus(); + const privyMobileLogout = usePrivyMobileLogout(); + + const { signIn } = useSignIn(); + + const user = useUserStore(); + + const walletRequest = usePrivyEthereumWalletRequest(); + const walletOn = usePrivyEthereumWalletOn(); + const signMessage = usePrivyMobileSignMessage(); + + const ethereumProvider = useCallback(async () => { + return { request: walletRequest, on: walletOn }; + }, [walletOn, walletRequest]); + + const signMessageProvider = useCallback( + async (message: string): Promise => { + return await signMessage(message); + }, + [signMessage], + ); + + useEffect(() => { + async function doAsync() { + if (!window.PRIVY_MOBILE_ENABLED) { + // only attempt to authenticate when running in the mobile app and + // privy is enabled. + return; + } + + if (user.isLoggedIn) { + // we're already authenticated so there's nothing to do... + return; + } + + const privyMobileAuthStatus = await getPrivyMobileAuthStatus({}); + + if (!privyMobileAuthStatus.enabled) { + return; + } + + if ( + !privyMobileAuthStatus.authenticated || + !privyMobileAuthStatus.userAuth + ) { + return; + } + + const webWallet = new PrivyEthereumWebWalletController( + ethereumProvider, + signMessageProvider, + ); + + await webWallet.enable(); + + const session = await getSessionFromWallet(webWallet, { + newSession: true, + }); + + const ssoProvider = privyMobileAuthStatus.userAuth.ssoProvider + ? toSignInProvider(privyMobileAuthStatus.userAuth.ssoProvider) + : undefined; + + const signInOpts = { + address: privyMobileAuthStatus.userAuth.address, + community_id: ChainBase.Ethereum, + wallet_id: WalletId.Privy, + privy: { + identityToken: privyMobileAuthStatus.userAuth.identityToken, + ssoOAuthToken: privyMobileAuthStatus.userAuth.ssoOAuthToken, + ssoProvider, + }, + }; + + console.log( + 'Going to authenticate with signInOpts: ' + + JSON.stringify(signInOpts, null, 2), + ); + await signIn(session, signInOpts); + + const landingURL = new URL( + '/dashboard/for-you', + window.location.href, + ).toString(); + document.location.href = landingURL; + } + + doAsync().catch((err) => { + console.error('Could not perform authentication: ' + err.message, err); + // FIXME enable this again once we have reliable authentication working... + //privyMobileLogout({}).catch(console.error); + }); + }, [ + user, + ethereumProvider, + getPrivyMobileAuthStatus, + signIn, + signMessageProvider, + privyMobileLogout, + ]); + + if (!user.isLoggedIn && window.PRIVY_MOBILE_ENABLED) { + return ; + } + + return children; +}; diff --git a/packages/commonwealth/client/scripts/views/components/PrivyMobile/types.ts b/packages/commonwealth/client/scripts/views/components/PrivyMobile/types.ts new file mode 100644 index 00000000000..fdbf6ee83d7 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/PrivyMobile/types.ts @@ -0,0 +1,23 @@ +import { WalletSsoSource } from '@hicommonwealth/shared'; + +/** + * When the user is authenticated, this provides the data the user needs to + * authenticate. + */ +export interface UserAuth { + /** + * The privy id which we're providing for debug info. It's not normally used + * otherwise. + */ + id: string; + address: string | null; + identityToken: string; + ssoOAuthToken: string; + ssoProvider: WalletSsoSource; +} + +export interface IPrivyAuthStatus { + enabled: boolean; + authenticated: boolean; + userAuth: UserAuth | null; +} diff --git a/packages/commonwealth/client/scripts/views/components/PrivyMobile/useNotificationsGetPermissionsAsyncReceiver.ts b/packages/commonwealth/client/scripts/views/components/PrivyMobile/useNotificationsGetPermissionsAsyncReceiver.ts new file mode 100644 index 00000000000..e350b06f736 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/PrivyMobile/useNotificationsGetPermissionsAsyncReceiver.ts @@ -0,0 +1,11 @@ +import { useMobileRPCSender } from 'hooks/mobile/useMobileRPCSender'; + +type PermissionStatus = { + status: 'granted' | 'denied' | 'undetermined'; +}; + +export function useNotificationsGetPermissionsAsyncReceiver() { + return useMobileRPCSender<{}, PermissionStatus>({ + type: 'Notifications.getPermissionsAsync', + }); +} diff --git a/packages/commonwealth/client/scripts/views/components/PrivyMobile/useNotificationsRequestPermissionsAsyncReceiver.ts b/packages/commonwealth/client/scripts/views/components/PrivyMobile/useNotificationsRequestPermissionsAsyncReceiver.ts new file mode 100644 index 00000000000..b3c5c77a9da --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/PrivyMobile/useNotificationsRequestPermissionsAsyncReceiver.ts @@ -0,0 +1,11 @@ +import { useMobileRPCSender } from 'client/scripts/hooks/mobile/useMobileRPCSender'; + +type PermissionStatus = { + status: 'granted' | 'denied' | 'undetermined'; +}; + +export function useNotificationsRequestPermissionsAsyncReceiver() { + return useMobileRPCSender<{}, PermissionStatus>({ + type: 'Notifications.requestPermissionsAsync', + }); +} diff --git a/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyEthereumWalletOn.ts b/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyEthereumWalletOn.ts new file mode 100644 index 00000000000..d3e3bc6f681 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyEthereumWalletOn.ts @@ -0,0 +1,5 @@ +import { useMobileRPCEventReceiver } from 'hooks/mobile/useMobileRPCEventReceiver'; + +export function usePrivyEthereumWalletOn() { + return useMobileRPCEventReceiver('privy.ethereumWalletOn'); +} diff --git a/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyEthereumWalletRequest.ts b/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyEthereumWalletRequest.ts new file mode 100644 index 00000000000..f0eb53fcea9 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyEthereumWalletRequest.ts @@ -0,0 +1,12 @@ +import { useMobileRPCSender } from 'hooks/mobile/useMobileRPCSender'; + +type RequestArguments = { + method: string; + params?: Array | undefined; +}; + +export function usePrivyEthereumWalletRequest() { + return useMobileRPCSender({ + type: 'privy.ethereumWalletRequest', + }); +} diff --git a/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyMobileAuthStatus.ts b/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyMobileAuthStatus.ts new file mode 100644 index 00000000000..bcd463666c1 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyMobileAuthStatus.ts @@ -0,0 +1,30 @@ +import { WalletSsoSource } from '@hicommonwealth/shared'; +import { useMobileRPCSender } from 'hooks/mobile/useMobileRPCSender'; + +/** + * When the user is authenticated, this provides the data the user needs to + * authenticate. + */ +export interface UserAuth { + /** + * The privy id which we're providing for debug info. It's not normally used + * otherwise. + */ + id: string; + address: string; + identityToken: string; + ssoOAuthToken?: string; + ssoProvider?: WalletSsoSource; +} + +export interface IPrivyMobileAuthStatus { + enabled: boolean; + authenticated: boolean; + userAuth: UserAuth | null; +} + +export function usePrivyMobileAuthStatus() { + return useMobileRPCSender<{}, IPrivyMobileAuthStatus>({ + type: 'privy.authStatus', + }); +} diff --git a/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyMobileAuthStatusStore.tsx b/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyMobileAuthStatusStore.tsx new file mode 100644 index 00000000000..1555acd6da9 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyMobileAuthStatusStore.tsx @@ -0,0 +1,25 @@ +import { createBoundedUseStore } from 'state/ui/utils'; +import { IPrivyAuthStatus } from 'views/components/PrivyMobile/types'; +import { devtools } from 'zustand/middleware'; +import { createStore } from 'zustand/vanilla'; + +type InternalState = { + status: IPrivyAuthStatus | undefined; +}; + +type SMSDialogStore = InternalState & { + setState: (state: InternalState) => void; +}; + +export const privyMobileAuthStatusStore = createStore()( + devtools((set) => ({ + status: undefined, + setState: (newState: InternalState) => set(newState), + })), +); + +const usePrivyMobileAuthStatusStore = createBoundedUseStore( + privyMobileAuthStatusStore, +); + +export default usePrivyMobileAuthStatusStore; diff --git a/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyMobileLogout.ts b/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyMobileLogout.ts new file mode 100644 index 00000000000..497dadedfc5 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyMobileLogout.ts @@ -0,0 +1,9 @@ +import { useMobileRPCSender } from 'hooks/mobile/useMobileRPCSender'; + +/** + * Get privy to sign a message, in react-native, then return the message into + * the browser. + */ +export function usePrivyMobileLogout() { + return useMobileRPCSender<{}, {}>({ type: 'privy.logout' }); +} diff --git a/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyMobileSignMessage.ts b/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyMobileSignMessage.ts new file mode 100644 index 00000000000..6dee18ca98b --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/PrivyMobile/usePrivyMobileSignMessage.ts @@ -0,0 +1,9 @@ +import { useMobileRPCSender } from 'hooks/mobile/useMobileRPCSender'; + +/** + * Get privy to sign a message, in react-native, then return the message into + * the browser. + */ +export function usePrivyMobileSignMessage() { + return useMobileRPCSender({ type: 'privy.signMessage' }); +} diff --git a/packages/commonwealth/client/scripts/views/components/ReactNativeBridge/ReactNativeBridgeRouter.tsx b/packages/commonwealth/client/scripts/views/components/ReactNativeBridge/ReactNativeBridgeRouter.tsx index 693167011c7..5555ccff770 100644 --- a/packages/commonwealth/client/scripts/views/components/ReactNativeBridge/ReactNativeBridgeRouter.tsx +++ b/packages/commonwealth/client/scripts/views/components/ReactNativeBridge/ReactNativeBridgeRouter.tsx @@ -1,6 +1,7 @@ import type { ReactNativeWebView } from 'hooks/useReactNativeWebView'; import { useCallback, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import { messageToObject } from './utils'; declare global { interface Window { @@ -62,16 +63,6 @@ export const ReactNativeBridgeRouter = () => { return null; }; -function messageToObject(message: string | object): object | null { - try { - return typeof message === 'string' ? JSON.parse(message) : message; - } catch (e) { - // this could happen if another library is sending non-JSON data via - // postMessage - return null; - } -} - function getPathAndQuery(url: string): string { // only navigate with the path and query because we don't want to include // the host portion as a notification could be from the official common.xyz diff --git a/packages/commonwealth/client/scripts/views/components/ReactNativeBridge/ReactNativeBridgeUser.tsx b/packages/commonwealth/client/scripts/views/components/ReactNativeBridge/ReactNativeBridgeUser.tsx index 9aa7ccf5772..4fdf1160d65 100644 --- a/packages/commonwealth/client/scripts/views/components/ReactNativeBridge/ReactNativeBridgeUser.tsx +++ b/packages/commonwealth/client/scripts/views/components/ReactNativeBridge/ReactNativeBridgeUser.tsx @@ -2,16 +2,7 @@ import useUserStore from 'client/scripts/state/ui/user'; import { useReactNativeWebView } from 'hooks/useReactNativeWebView'; import { useEffect, useState } from 'react'; import { useDarkMode } from '../../../state/ui/darkMode/darkMode'; - -/** - * Typed message so that the react-native client knows how to handel this message. - * - * This is teh standard pattern of how to handle postMessage with multiple uses. - */ -type TypedData = { - type: string; - data: Data; -}; +import { TypedData } from './types'; /** * The actual user info that the client needs. diff --git a/packages/commonwealth/client/scripts/views/components/ReactNativeBridge/types.ts b/packages/commonwealth/client/scripts/views/components/ReactNativeBridge/types.ts new file mode 100644 index 00000000000..97bd931c827 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/ReactNativeBridge/types.ts @@ -0,0 +1,14 @@ +/** + * Typed message so that the react-native client knows how to handel this message. + * + * This is teh standard pattern of how to handle postMessage with multiple uses. + */ +export type TypedData = { + type: string; + data: Data; +}; + +export interface ReactNativeWebView { + // allows us to send messages to ReactNative. + postMessage: (message: string) => void; +} diff --git a/packages/commonwealth/client/scripts/views/components/ReactNativeBridge/utils.ts b/packages/commonwealth/client/scripts/views/components/ReactNativeBridge/utils.ts new file mode 100644 index 00000000000..314081ea7ef --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/ReactNativeBridge/utils.ts @@ -0,0 +1,9 @@ +export function messageToObject(message: string | object): object | null { + try { + return typeof message === 'string' ? JSON.parse(message) : message; + } catch (e) { + // this could happen if another library is sending non-JSON data via + // postMessage + return null; + } +} diff --git a/packages/commonwealth/client/scripts/views/components/SublayoutHeader/useUserMenuItems.tsx b/packages/commonwealth/client/scripts/views/components/SublayoutHeader/useUserMenuItems.tsx index 5c6dcb8add7..21f7251fc8b 100644 --- a/packages/commonwealth/client/scripts/views/components/SublayoutHeader/useUserMenuItems.tsx +++ b/packages/commonwealth/client/scripts/views/components/SublayoutHeader/useUserMenuItems.tsx @@ -33,6 +33,7 @@ import useUserStore from 'state/ui/user'; import { PopoverMenuItem } from 'views/components/component_kit/CWPopoverMenu'; import { CWToggle } from 'views/components/component_kit/new_designs/cw_toggle'; import CWIconButton from 'views/components/component_kit/new_designs/CWIconButton'; +import { usePrivyMobileLogout } from 'views/components/PrivyMobile/usePrivyMobileLogout'; import useAuthentication from '../../modals/AuthModal/useAuthentication'; import { MobileTabType } from '../../pages/WalletPage/types'; import { mobileTabParam } from '../../pages/WalletPage/utils'; @@ -79,6 +80,7 @@ const useUserMenuItems = ({ const privyEnabled = useFlag('privy'); const { authenticated, logout } = usePrivy(); + const privyMobileLogout = usePrivyMobileLogout(); const userData = useUserStore(); const hasMagic = userData.hasMagicWallet; @@ -116,6 +118,11 @@ const useUserMenuItems = ({ if (privyEnabled && authenticated) { await logout(); } + + // it's ok to call this when running outside of the mobile app as nothing + // will happen. + privyMobileLogout({}).catch(console.error); + notifySuccess('Signed out'); darkModeStore.getState().setDarkMode(false); setLocalStorageItem(LocalStorageKeys.HasSeenNotifications, 'true'); @@ -124,7 +131,7 @@ const useUserMenuItems = ({ notifyError('Something went wrong during logging out.'); window.location.reload(); } - }, [authenticated, logout, privyEnabled]); + }, [authenticated, logout, privyEnabled, privyMobileLogout]); useEffect(() => { // if a user is in a stake enabled community without membership, set first user address as active that diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Groups/Update/UpdateCommunityGroupPage.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Groups/Update/UpdateCommunityGroupPage.tsx index f2b87d93e53..5b58c1157f6 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Groups/Update/UpdateCommunityGroupPage.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityGroupsAndMembers/Groups/Update/UpdateCommunityGroupPage.tsx @@ -10,8 +10,8 @@ import useUserStore from 'state/ui/user'; import Permissions from 'utils/Permissions'; import { MixpanelPageViewEvent } from '../../../../../../../shared/analytics/types'; import useAppStatus from '../../../../../hooks/useAppStatus'; +import { LoadingIndicator } from '../../../../components/LoadingIndicator/LoadingIndicator'; import { PageNotFound } from '../../../404'; -import { PageLoading } from '../../../loading'; import { AMOUNT_CONDITIONS, chainTypes, @@ -77,7 +77,7 @@ const UpdateCommunityGroupPage = ({ groupId }: { groupId: string }) => { } if (isLoading) { - return ; + return ; } return ( diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Discord/CallbackPage.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Discord/CallbackPage.tsx index 63ac482855a..1be47040868 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Discord/CallbackPage.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Integrations/Discord/CallbackPage.tsx @@ -2,8 +2,8 @@ import useNecessaryEffect from 'hooks/useNecessaryEffect'; import { useCommonNavigate } from 'navigation/helpers'; import React, { useState } from 'react'; import { useSetDiscordBotConfigMutation } from 'state/api/discord'; +import { LoadingIndicator } from '../../../../components/LoadingIndicator/LoadingIndicator'; import { PageNotFound } from '../../../404'; -import { PageLoading } from '../../../loading'; const CallbackPage = () => { const navigate = useCommonNavigate(); @@ -59,7 +59,7 @@ const CallbackPage = () => { return failed ? ( ) : ( - + ); }; diff --git a/packages/commonwealth/client/scripts/views/pages/GovernancePage/GovernancePage.tsx b/packages/commonwealth/client/scripts/views/pages/GovernancePage/GovernancePage.tsx index e9d251127a2..b266328cc27 100644 --- a/packages/commonwealth/client/scripts/views/pages/GovernancePage/GovernancePage.tsx +++ b/packages/commonwealth/client/scripts/views/pages/GovernancePage/GovernancePage.tsx @@ -9,8 +9,8 @@ import { } from 'client/scripts/state/api/proposals'; import React, { useEffect, useState } from 'react'; import CWPageLayout from '../../components/component_kit/new_designs/CWPageLayout'; +import { LoadingIndicator } from '../../components/LoadingIndicator/LoadingIndicator'; import { PageNotFound } from '../404'; -import { PageLoading } from '../loading'; import GovernanceCards from './GovernanceCards'; import GovernanceHeader from './GovernanceHeader/GovernanceHeader'; import './GovernancePage.scss'; @@ -60,7 +60,7 @@ const GovernancePage = () => { ); } - return ; + return ; } const activeProposalsCount = activeCosmosProposals?.length || 0; diff --git a/packages/commonwealth/client/scripts/views/pages/NewProposalViewPage/NewProposalViewPage.tsx b/packages/commonwealth/client/scripts/views/pages/NewProposalViewPage/NewProposalViewPage.tsx index 4fa7eb6efe2..7685134a486 100644 --- a/packages/commonwealth/client/scripts/views/pages/NewProposalViewPage/NewProposalViewPage.tsx +++ b/packages/commonwealth/client/scripts/views/pages/NewProposalViewPage/NewProposalViewPage.tsx @@ -10,6 +10,7 @@ import { useSearchParams } from 'react-router-dom'; import app from 'state'; import CWPageLayout from 'views/components/component_kit/new_designs/CWPageLayout'; import useManageDocumentTitle from '../../../hooks/useManageDocumentTitle'; +import { LoadingIndicator } from '../../components/LoadingIndicator/LoadingIndicator'; import MarkdownViewerWithFallback from '../../components/MarkdownViewerWithFallback'; import CWAccordView from '../../components/component_kit/CWAccordView/CWAccordView'; import { CWContentPage } from '../../components/component_kit/CWContentPage'; @@ -24,7 +25,6 @@ import { VotingActions } from '../../components/proposals/voting_actions'; import { VotingResults } from '../../components/proposals/voting_results'; import { PageNotFound } from '../404'; import { SnapshotPollCardContainer } from '../Snapshots/ViewSnapshotProposal/SnapshotPollCard'; -import { PageLoading } from '../loading'; import { JSONDisplay } from '../view_proposal/JSONDisplay'; import ProposalVotesDrawer from './ProposalVotesDrawer/ProposalVotesDrawer'; import { useCosmosProposal } from './useCosmosProposal'; @@ -136,7 +136,7 @@ const NewProposalViewPage = ({ identifier, scope }: ViewProposalPageProps) => { }, [snapshotProposal, proposal, queryType]); if (isLoading || isSnapshotLoading) { - return ; + return ; } if (cosmosError) { diff --git a/packages/commonwealth/client/scripts/views/pages/NotificationSettings/index.tsx b/packages/commonwealth/client/scripts/views/pages/NotificationSettings/index.tsx index 91e580bf764..9ea50490919 100644 --- a/packages/commonwealth/client/scripts/views/pages/NotificationSettings/index.tsx +++ b/packages/commonwealth/client/scripts/views/pages/NotificationSettings/index.tsx @@ -2,12 +2,12 @@ import { CommunityAlert } from '@hicommonwealth/schemas'; import React, { useState } from 'react'; import { useCommunityAlertsQuery } from 'state/api/trpc/subscription/useCommunityAlertsQuery'; import useUserStore from 'state/ui/user'; -import ScrollContainer from 'views/components/ScrollContainer'; import CWPageLayout from 'views/components/component_kit/new_designs/CWPageLayout'; import { CWTab, CWTabsRow, } from 'views/components/component_kit/new_designs/CWTabs'; +import ScrollContainer from 'views/components/ScrollContainer'; import { PageNotFound } from 'views/pages/404'; import { CommentSubscriptions } from 'views/pages/NotificationSettings/CommentSubscriptions'; import { CommunityEntry } from 'views/pages/NotificationSettings/CommunityEntry'; @@ -19,7 +19,7 @@ import { useSupportsPushNotifications } from 'views/pages/NotificationSettings/u import { useThreadSubscriptions } from 'views/pages/NotificationSettings/useThreadSubscriptions'; import { z } from 'zod'; import { CWText } from '../../components/component_kit/cw_text'; -import { PageLoading } from '../loading'; +import { LoadingIndicator } from '../../components/LoadingIndicator/LoadingIndicator'; import './index.scss'; type NotificationSection = @@ -42,7 +42,7 @@ const NotificationSettings = () => { useState('push-notifications'); if (threadSubscriptions.isLoading) { - return ; + return ; } else if (!user.isLoggedIn) { return ; } diff --git a/packages/commonwealth/client/scripts/views/pages/NotificationSettings/useSubscriptionPreferenceSettingToggle.ts b/packages/commonwealth/client/scripts/views/pages/NotificationSettings/useSubscriptionPreferenceSettingToggle.ts index 7206ac35f71..ff3adebc2fb 100644 --- a/packages/commonwealth/client/scripts/views/pages/NotificationSettings/useSubscriptionPreferenceSettingToggle.ts +++ b/packages/commonwealth/client/scripts/views/pages/NotificationSettings/useSubscriptionPreferenceSettingToggle.ts @@ -6,13 +6,16 @@ import { useUpdateSubscriptionPreferencesMutation } from 'state/api/trpc/subscri // eslint-disable-next-line max-len import useUserStore from 'state/ui/user'; // eslint-disable-next-line max-len +import { useNotificationsRequestPermissionsAsyncReceiver } from 'views/components/PrivyMobile/useNotificationsRequestPermissionsAsyncReceiver'; import { SubscriptionPrefType } from 'views/pages/NotificationSettings/useSubscriptionPreferenceSetting'; -import { verifyMobileNotificationPermissions } from './verifyMobileNotificationPermissions'; export function useSubscriptionPreferenceSettingToggle( prefs: SubscriptionPrefType[], ) { const subscriptionPreferences = useSubscriptionPreferences(); + + const requestPermissions = useNotificationsRequestPermissionsAsyncReceiver(); + const { mutateAsync: updateSubscriptionPreferences } = useUpdateSubscriptionPreferencesMutation(); const user = useUserStore(); @@ -21,8 +24,10 @@ export function useSubscriptionPreferenceSettingToggle( async (activate: boolean) => { if (activate) { // *** we have to first request permissions if we're activating. - const verified = await verifyMobileNotificationPermissions(); - if (!verified) { + const { status: notificationPermissions } = await requestPermissions( + {}, + ); + if (notificationPermissions !== 'granted') { return; } } @@ -46,6 +51,12 @@ export function useSubscriptionPreferenceSettingToggle( await subscriptionPreferences.refetch(); }, - [prefs, subscriptionPreferences, updateSubscriptionPreferences, user.id], + [ + prefs, + requestPermissions, + subscriptionPreferences, + updateSubscriptionPreferences, + user.id, + ], ); } diff --git a/packages/commonwealth/client/scripts/views/pages/NotificationSettings/verifyMobileNotificationPermissions.ts b/packages/commonwealth/client/scripts/views/pages/NotificationSettings/verifyMobileNotificationPermissions.ts index 6d8d0478c52..a7f39b583e4 100644 --- a/packages/commonwealth/client/scripts/views/pages/NotificationSettings/verifyMobileNotificationPermissions.ts +++ b/packages/commonwealth/client/scripts/views/pages/NotificationSettings/verifyMobileNotificationPermissions.ts @@ -1,6 +1,9 @@ import { isMobileApp } from 'hooks/useReactNativeWebView'; import { MobileNotifications } from 'utils/MobileNotifications'; +/** + * @deprecated Not used any longer. We should remove. + */ export async function verifyMobileNotificationPermissions(): Promise { if (isMobileApp()) { const existingPermissions = await MobileNotifications.getPermissionsAsync(); diff --git a/packages/commonwealth/client/scripts/views/pages/Redirects/GroupRedirect.tsx b/packages/commonwealth/client/scripts/views/pages/Redirects/GroupRedirect.tsx index 2b46505a281..764026becd8 100644 --- a/packages/commonwealth/client/scripts/views/pages/Redirects/GroupRedirect.tsx +++ b/packages/commonwealth/client/scripts/views/pages/Redirects/GroupRedirect.tsx @@ -2,7 +2,7 @@ import useRunOnceOnCondition from 'hooks/useRunOnceOnCondition'; import { useCommonNavigate } from 'navigation/helpers'; import React from 'react'; import { useFetchGroupsQuery } from 'state/api/groups'; -import { PageLoading } from '../loading'; +import { LoadingIndicator } from '../../components/LoadingIndicator/LoadingIndicator'; const GroupRedirect = ({ id }: { id: string }) => { const navigate = useCommonNavigate(); @@ -26,7 +26,7 @@ const GroupRedirect = ({ id }: { id: string }) => { shouldRun: !!(group || error), }); - return ; + return ; }; export default GroupRedirect; diff --git a/packages/commonwealth/client/scripts/views/pages/comment_redirect.tsx b/packages/commonwealth/client/scripts/views/pages/comment_redirect.tsx index 47847faddd9..d8c1d3bb14a 100644 --- a/packages/commonwealth/client/scripts/views/pages/comment_redirect.tsx +++ b/packages/commonwealth/client/scripts/views/pages/comment_redirect.tsx @@ -2,7 +2,7 @@ import useRunOnceOnCondition from 'hooks/useRunOnceOnCondition'; import { useCommonNavigate } from 'navigation/helpers'; import React from 'react'; import { useFetchCommentsQuery } from 'state/api/comments'; -import { PageLoading } from './loading'; +import { LoadingIndicator } from '../components/LoadingIndicator/LoadingIndicator'; const CommentRedirect = ({ identifier }: { identifier: string }) => { const navigate = useCommonNavigate(); @@ -32,7 +32,7 @@ const CommentRedirect = ({ identifier }: { identifier: string }) => { shouldRun: !!(foundComment || error), }); - return ; + return ; }; export default CommentRedirect; diff --git a/packages/commonwealth/client/scripts/views/pages/discussions_redirect.tsx b/packages/commonwealth/client/scripts/views/pages/discussions_redirect.tsx index bf0169031b9..1d9849e5f3e 100644 --- a/packages/commonwealth/client/scripts/views/pages/discussions_redirect.tsx +++ b/packages/commonwealth/client/scripts/views/pages/discussions_redirect.tsx @@ -5,7 +5,7 @@ import { NavigateOptions, useLocation } from 'react-router-dom'; import { DefaultPage } from '@hicommonwealth/shared'; import { useCommonNavigate } from 'navigation/helpers'; import app from 'state'; -import { PageLoading } from './loading'; +import { LoadingIndicator } from '../components/LoadingIndicator/LoadingIndicator'; export default function DiscussionsRedirect() { const navigate = useCommonNavigate(); @@ -40,5 +40,5 @@ export default function DiscussionsRedirect() { } }, [navigate, location.search]); - return ; + return ; } diff --git a/packages/commonwealth/client/scripts/views/pages/finish_social_login.tsx b/packages/commonwealth/client/scripts/views/pages/finish_social_login.tsx index 098e8288907..3aad87ffa4f 100644 --- a/packages/commonwealth/client/scripts/views/pages/finish_social_login.tsx +++ b/packages/commonwealth/client/scripts/views/pages/finish_social_login.tsx @@ -5,8 +5,8 @@ import React, { useEffect, useState } from 'react'; import { initAppState } from 'state'; import { useFetchCustomDomainQuery } from 'state/api/configuration'; import useUserStore from 'state/ui/user'; +import { LoadingIndicator } from 'views/components/LoadingIndicator/LoadingIndicator'; import ErrorPage from 'views/pages/error'; -import { PageLoading } from 'views/pages/loading'; const validate = async ( setRoute: (route: string) => void, @@ -102,7 +102,7 @@ const FinishSocialLogin = () => { if (validationError) { return ; } else { - return ; + return ; } }; diff --git a/packages/commonwealth/client/scripts/views/pages/loading.tsx b/packages/commonwealth/client/scripts/views/pages/loading.tsx deleted file mode 100644 index 9e518af4744..00000000000 --- a/packages/commonwealth/client/scripts/views/pages/loading.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import { CWText } from '../components/component_kit/cw_text'; -import CWCircleMultiplySpinner from '../components/component_kit/new_designs/CWCircleMultiplySpinner'; -import './loading.scss'; - -type PageLoadingProps = { - message?: string; -}; - -export const PageLoading = (props: PageLoadingProps) => { - const { message } = props; - - return ( -
-
- - {message} -
-
- ); -}; diff --git a/packages/commonwealth/client/scripts/views/pages/new_proposal/index.tsx b/packages/commonwealth/client/scripts/views/pages/new_proposal/index.tsx index a1f5faf1d68..845f4474f08 100644 --- a/packages/commonwealth/client/scripts/views/pages/new_proposal/index.tsx +++ b/packages/commonwealth/client/scripts/views/pages/new_proposal/index.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'; import app from 'state'; import { userStore } from 'state/ui/user'; import CWPageLayout from 'views/components/component_kit/new_designs/CWPageLayout'; -import { PageLoading } from 'views/pages/loading'; +import { LoadingIndicator } from 'views/components/LoadingIndicator/LoadingIndicator'; import { CWText } from '../../components/component_kit/cw_text'; import { PageNotFound } from '../404'; import { CosmosProposalForm } from './cosmos_proposal_form'; @@ -34,7 +34,7 @@ const NewProposalPage = () => { } if (!app.chain || !isLoaded || !app.chain.meta) { - return ; + return ; } // special case for initializing cosmos governance @@ -48,7 +48,7 @@ const NewProposalPage = () => { app.chainModuleReady.emit('ready'); }); } - return ; + return ; } } else { return ; diff --git a/packages/commonwealth/client/scripts/views/pages/profile_redirect.tsx b/packages/commonwealth/client/scripts/views/pages/profile_redirect.tsx index d262050c6ff..feffa6cf595 100644 --- a/packages/commonwealth/client/scripts/views/pages/profile_redirect.tsx +++ b/packages/commonwealth/client/scripts/views/pages/profile_redirect.tsx @@ -5,8 +5,8 @@ import { useCommonNavigate } from 'navigation/helpers'; import app from 'state'; import { useFetchProfilesByAddressesQuery } from 'state/api/profiles'; import useUserStore from 'state/ui/user'; +import { LoadingIndicator } from '../components/LoadingIndicator/LoadingIndicator'; import { PageNotFound } from './404'; -import { PageLoading } from './loading'; type ProfileRedirectProps = { address: string; @@ -41,7 +41,7 @@ const ProfileRedirect = (props: ProfileRedirectProps) => { }, [isError, users]); if (isLoading) { - return ; + return ; } if (isError) { diff --git a/packages/commonwealth/client/scripts/views/pages/proposals.tsx b/packages/commonwealth/client/scripts/views/pages/proposals.tsx index 7ed9f6b39bb..aab0158ad64 100644 --- a/packages/commonwealth/client/scripts/views/pages/proposals.tsx +++ b/packages/commonwealth/client/scripts/views/pages/proposals.tsx @@ -11,10 +11,10 @@ import { useActiveCosmosProposalsQuery, useCompletedCosmosProposalsQuery, } from 'state/api/proposals'; +import { LoadingIndicator } from 'views/components/LoadingIndicator/LoadingIndicator'; import { ProposalCard } from 'views/components/ProposalCard'; import CWPageLayout from 'views/components/component_kit/new_designs/CWPageLayout'; import { PageNotFound } from 'views/pages/404'; -import { PageLoading } from 'views/pages/loading'; import useManageDocumentTitle from '../../hooks/useManageDocumentTitle'; import { CardsCollection } from '../components/cards_collection'; import { CWText } from '../components/component_kit/cw_text'; @@ -76,7 +76,7 @@ const ProposalsPage = () => { ); } - return ; + return ; } const activeProposalContent = isLoadingCosmosActiveProposals ? ( diff --git a/packages/commonwealth/client/scripts/views/pages/search/index.tsx b/packages/commonwealth/client/scripts/views/pages/search/index.tsx index a5c60b0448d..456db9a8309 100644 --- a/packages/commonwealth/client/scripts/views/pages/search/index.tsx +++ b/packages/commonwealth/client/scripts/views/pages/search/index.tsx @@ -16,7 +16,7 @@ import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import app from 'state'; import { useFetchCustomDomainQuery } from 'state/api/configuration'; import CWPageLayout from 'views/components/component_kit/new_designs/CWPageLayout'; -import { PageLoading } from 'views/pages/loading'; +import { LoadingIndicator } from 'views/components/LoadingIndicator/LoadingIndicator'; import { APIOrderBy, APIOrderDirection, @@ -309,7 +309,7 @@ const SearchPage = () => { {sharedQueryOptions?.searchTerm?.length > 0 && ( <> {isLoading ? ( - + ) : ( <> diff --git a/packages/commonwealth/client/scripts/views/pages/snapshot_proposal_link_redirect.tsx b/packages/commonwealth/client/scripts/views/pages/snapshot_proposal_link_redirect.tsx index afee64842a9..f376a0fde1e 100644 --- a/packages/commonwealth/client/scripts/views/pages/snapshot_proposal_link_redirect.tsx +++ b/packages/commonwealth/client/scripts/views/pages/snapshot_proposal_link_redirect.tsx @@ -2,7 +2,7 @@ import useNecessaryEffect from 'hooks/useNecessaryEffect'; import { useCommonNavigate } from 'navigation/helpers'; import React from 'react'; import { getSnapshotProposalQuery } from 'state/api/snapshots'; -import { PageLoading } from './loading'; +import { LoadingIndicator } from '../components/LoadingIndicator/LoadingIndicator'; type SnapshotProposalLinkRedirectProps = { identifier: string; @@ -41,7 +41,7 @@ const SnapshotProposalLinkRedirect = ({ fetchSnapshotData(); }, [navigate]); - return ; + return ; }; export default SnapshotProposalLinkRedirect; diff --git a/packages/commonwealth/client/scripts/views/pages/stats.tsx b/packages/commonwealth/client/scripts/views/pages/stats.tsx index 692e631d527..8722ec90239 100644 --- a/packages/commonwealth/client/scripts/views/pages/stats.tsx +++ b/packages/commonwealth/client/scripts/views/pages/stats.tsx @@ -5,8 +5,8 @@ import app from 'state'; import { SERVER_URL } from 'state/api/config'; import useUserStore, { userStore } from 'state/ui/user'; import CWPageLayout from 'views/components/component_kit/new_designs/CWPageLayout'; +import { LoadingIndicator } from 'views/components/LoadingIndicator/LoadingIndicator'; import ErrorPage from 'views/pages/error'; -import { PageLoading } from 'views/pages/loading'; import { CWText } from '../components/component_kit/cw_text'; import { PageNotFound } from './404'; import './stats.scss'; @@ -155,7 +155,7 @@ const StatsPage = () => { } if (!batchedData) { - return ; + return ; } else if (error) { return ; } diff --git a/packages/commonwealth/client/scripts/views/pages/thread_redirect.tsx b/packages/commonwealth/client/scripts/views/pages/thread_redirect.tsx index b841d05f770..bee38ec94e8 100644 --- a/packages/commonwealth/client/scripts/views/pages/thread_redirect.tsx +++ b/packages/commonwealth/client/scripts/views/pages/thread_redirect.tsx @@ -2,7 +2,7 @@ import useRunOnceOnCondition from 'hooks/useRunOnceOnCondition'; import { useCommonNavigate } from 'navigation/helpers'; import React from 'react'; import { useGetThreadsByIdQuery } from 'state/api/threads'; -import { PageLoading } from './loading'; +import { LoadingIndicator } from '../components/LoadingIndicator/LoadingIndicator'; const ThreadRedirect = ({ identifier }: { identifier: string }) => { const navigate = useCommonNavigate(); @@ -27,7 +27,7 @@ const ThreadRedirect = ({ identifier }: { identifier: string }) => { shouldRun: !!(foundThread || error), }); - return ; + return ; }; export default ThreadRedirect; diff --git a/packages/commonwealth/client/scripts/views/pages/topic_redirect.tsx b/packages/commonwealth/client/scripts/views/pages/topic_redirect.tsx index f397effe8bd..44dab210446 100644 --- a/packages/commonwealth/client/scripts/views/pages/topic_redirect.tsx +++ b/packages/commonwealth/client/scripts/views/pages/topic_redirect.tsx @@ -3,7 +3,7 @@ import useRunOnceOnCondition from 'hooks/useRunOnceOnCondition'; import { useCommonNavigate } from 'navigation/helpers'; import React from 'react'; import { useGetTopicByIdQuery } from 'state/api/topics'; -import { PageLoading } from './loading'; +import { LoadingIndicator } from '../components/LoadingIndicator/LoadingIndicator'; const ThreadRedirect = ({ id }: { id: number }) => { const navigate = useCommonNavigate(); @@ -46,7 +46,7 @@ const ThreadRedirect = ({ id }: { id: number }) => { shouldRun: !!(topic || error), }); - return ; + return ; }; export default ThreadRedirect; diff --git a/packages/commonwealth/client/scripts/views/pages/view_proposal/index.tsx b/packages/commonwealth/client/scripts/views/pages/view_proposal/index.tsx index 214e9e765df..a37a337c535 100644 --- a/packages/commonwealth/client/scripts/views/pages/view_proposal/index.tsx +++ b/packages/commonwealth/client/scripts/views/pages/view_proposal/index.tsx @@ -16,15 +16,15 @@ import { useCosmosProposalVotesQuery, } from 'state/api/proposals'; import CWPageLayout from 'views/components/component_kit/new_designs/CWPageLayout'; -import { PageLoading } from 'views/pages/loading'; +import { LoadingIndicator } from 'views/components/LoadingIndicator/LoadingIndicator'; import useManageDocumentTitle from '../../../hooks/useManageDocumentTitle'; import type { AnyProposal } from '../../../models/types'; -import MarkdownViewerWithFallback from '../../components/MarkdownViewerWithFallback'; -import { Skeleton } from '../../components/Skeleton'; import CWAccordView from '../../components/component_kit/CWAccordView/CWAccordView'; import { CWContentPage } from '../../components/component_kit/CWContentPage'; +import MarkdownViewerWithFallback from '../../components/MarkdownViewerWithFallback'; import TimeLineCard from '../../components/proposals/TimeLineCard'; import { VotingResults } from '../../components/proposals/voting_results'; +import { Skeleton } from '../../components/Skeleton'; import { PageNotFound } from '../404'; import { JSONDisplay } from './JSONDisplay'; import { ProposalSubheader } from './proposal_components'; @@ -95,7 +95,7 @@ const ViewProposalPage = ({ identifier }: ViewProposalPageAttrs) => { }, [isAdapterLoaded, proposalId]); if (isFetchingProposal || !isAdapterLoaded) { - return ; + return ; } if (cosmosError) {