diff --git a/packages/common/src/messages/settings.ts b/packages/common/src/messages/settings.ts index 911ad5fa0be..f933cc8b6d4 100644 --- a/packages/common/src/messages/settings.ts +++ b/packages/common/src/messages/settings.ts @@ -25,6 +25,7 @@ export const settingsMessages = { changeEmailCardTitle: 'Change Email', changePasswordCardTitle: 'Change Password', accountsYouManageTitle: 'Accounts You Manage', + verificationCardTitle: 'Verification', desktopAppCardTitle: 'Download the Desktop App', appearanceDescription: @@ -39,11 +40,14 @@ export const settingsMessages = { changeEmailCardDescription: 'Change the email you use to sign in and receive emails.', changePasswordCardDescription: 'Change the password to your Audius account.', + verificationCardDescription: + 'Verify your Audius profile by completing identity verification', desktopAppCardDescription: 'For the best experience, we recommend downloading the Audius App.', labelAccountCardDescription: 'Identify as a record label on your Audius profile.', + verificationCardButtonText: 'Get Verified', inboxSettingsButtonText: 'Inbox Settings', commentSettingsButtonText: 'Comment Settings', notificationsButtonText: 'Configure Notifications', diff --git a/packages/common/src/services/auth/identity.ts b/packages/common/src/services/auth/identity.ts index 469f5bf31bf..65924129f87 100644 --- a/packages/common/src/services/auth/identity.ts +++ b/packages/common/src/services/auth/identity.ts @@ -57,8 +57,20 @@ export class IdentityService { this.getAudiusWalletClient = audiusWalletClient } - // #region: Internal Functions - private async _getSignatureHeaders() { + async getAuthHeaders() { + // Check if auth headers are provided in localStorage (e.g., from mobile WebView) + // This allows mobile apps to inject auth headers for web authentication + if (typeof window !== 'undefined' && window.localStorage) { + const storedMessage = window.localStorage.getItem(AuthHeaders.Message) + const storedSignature = window.localStorage.getItem(AuthHeaders.Signature) + if (storedMessage && storedSignature) { + return { + [AuthHeaders.Message]: storedMessage, + [AuthHeaders.Signature]: storedSignature + } + } + } + const audiusWalletClient = await this.getAudiusWalletClient() const [currentAddress] = await audiusWalletClient.getAddresses() if (!currentAddress) { @@ -112,12 +124,10 @@ export class IdentityService { } } - // #region: Public Functions - async sendRecoveryInfo(args: RecoveryInfoParams) { // This endpoint takes data/signature as body params const { [AuthHeaders.Message]: data, [AuthHeaders.Signature]: signature } = - await this._getSignatureHeaders() + await this.getAuthHeaders() return await this._makeRequest<{ status: true }>({ url: '/recovery', method: 'post', @@ -208,7 +218,7 @@ export class IdentityService { * Get the user's email used for notifications and display. */ async getUserEmail() { - const headers = await this._getSignatureHeaders() + const headers = await this.getAuthHeaders() const res = await this._makeRequest<{ email: string | undefined | null }>({ url: '/user/email', @@ -226,7 +236,7 @@ export class IdentityService { * Change the user's email used for notifications and display. */ async changeEmail({ email, otp }: { email: string; otp?: string }) { - const headers = await this._getSignatureHeaders() + const headers = await this.getAuthHeaders() return await this._makeRequest({ url: '/user/email', @@ -239,7 +249,7 @@ export class IdentityService { async createStripeSession( data: CreateStripeSessionRequest ): Promise { - const headers = await this._getSignatureHeaders() + const headers = await this.getAuthHeaders() return await this._makeRequest({ url: '/stripe/session', @@ -250,7 +260,7 @@ export class IdentityService { } async recordIP() { - const headers = await this._getSignatureHeaders() + const headers = await this.getAuthHeaders() return await this._makeRequest({ url: '/record_ip', @@ -260,7 +270,7 @@ export class IdentityService { } async getUserBankTransactionMetadata(transactionId: string) { - const headers = await this._getSignatureHeaders() + const headers = await this.getAuthHeaders() const metadatas = await this._makeRequest< Array<{ metadata: InAppAudioPurchaseMetadata }> @@ -276,7 +286,7 @@ export class IdentityService { transactionSignature: string metadata: InAppAudioPurchaseMetadata }) { - const headers = await this._getSignatureHeaders() + const headers = await this.getAuthHeaders() return await this._makeRequest({ url: '/transaction_metadata', @@ -287,7 +297,7 @@ export class IdentityService { } async createPlaidLinkToken() { - const headers = await this._getSignatureHeaders() + const headers = await this.getAuthHeaders() return await this._makeRequest<{ linkToken: string }>({ url: '/create_link_token', diff --git a/packages/common/src/store/ui/modals/parentSlice.ts b/packages/common/src/store/ui/modals/parentSlice.ts index 91f16477fe5..7be766d592a 100644 --- a/packages/common/src/store/ui/modals/parentSlice.ts +++ b/packages/common/src/store/ui/modals/parentSlice.ts @@ -81,7 +81,9 @@ export const initialState: BasicModalsState = { ReceiveTokensModal: { isOpen: false }, SendTokensModal: { isOpen: false }, CoinSuccessModal: { isOpen: false }, - ArtistCoinDetailsModal: { isOpen: false } + ArtistCoinDetailsModal: { isOpen: false }, + VerificationSuccess: { isOpen: false }, + VerificationError: { isOpen: false } } const slice = createSlice({ diff --git a/packages/common/src/store/ui/modals/types.ts b/packages/common/src/store/ui/modals/types.ts index 57bd99b433e..d655adad841 100644 --- a/packages/common/src/store/ui/modals/types.ts +++ b/packages/common/src/store/ui/modals/types.ts @@ -116,6 +116,8 @@ export type Modals = | 'ArtistCoinDetailsModal' | 'FinalizeWinnersConfirmation' | 'CoinSuccessModal' + | 'VerificationSuccess' + | 'VerificationError' export type BasicModalsState = { [modal in Modals]: BaseModalState diff --git a/packages/mobile/src/app/Drawers.tsx b/packages/mobile/src/app/Drawers.tsx index b74fc692fcd..95ea830e0fb 100644 --- a/packages/mobile/src/app/Drawers.tsx +++ b/packages/mobile/src/app/Drawers.tsx @@ -45,6 +45,8 @@ import { StripeOnrampDrawer } from 'app/components/stripe-onramp-drawer' import { SupportersInfoDrawer } from 'app/components/supporters-info-drawer' import { TransferAudioMobileDrawer } from 'app/components/transfer-audio-mobile-drawer' import { TrendingRewardsDrawer } from 'app/components/trending-rewards-drawer' +import { VerificationErrorDrawer } from 'app/components/verification-error-drawer/VerificationErrorDrawer' +import { VerificationSuccessDrawer } from 'app/components/verification-success-drawer/VerificationSuccessDrawer' import { WaitForDownloadDrawer } from 'app/components/wait-for-download-drawer' import { WithdrawUSDCDrawer } from 'app/components/withdraw-usdc-drawer/WithdrawUSDCDrawer' import { CoinInsightsOverflowMenu } from 'app/screens/coin-details-screen/components/CoinInsightsOverflowMenu' @@ -142,7 +144,9 @@ const commonDrawersMap: { [Modal in Modals]?: ComponentType } = { WithdrawUSDCModal: WithdrawUSDCDrawer, ReceiveTokensModal: ReceiveTokensDrawer, SendTokensModal: SendTokensDrawer, - ArtistCoinDetailsModal: ArtistCoinDetailsDrawer + ArtistCoinDetailsModal: ArtistCoinDetailsDrawer, + VerificationSuccess: VerificationSuccessDrawer, + VerificationError: VerificationErrorDrawer } const nativeDrawersMap: { [DrawerName in Drawer]?: ComponentType } = { diff --git a/packages/mobile/src/components/core/TextInput.tsx b/packages/mobile/src/components/core/TextInput.tsx index 7bf6c168e3e..b9044704d48 100644 --- a/packages/mobile/src/components/core/TextInput.tsx +++ b/packages/mobile/src/components/core/TextInput.tsx @@ -34,8 +34,8 @@ import { IconCloseAlt, useTheme } from '@audius/harmony-native' -import { usePressScaleAnimation } from 'app/hooks/usePressScaleAnimation' import { TextInputAccessoryView } from 'app/harmony-native/components/input/TextInput/TextInputAccessoryView' +import { usePressScaleAnimation } from 'app/hooks/usePressScaleAnimation' import type { StylesProp } from 'app/styles' import { makeStyles } from 'app/styles' import { spacing } from 'app/styles/spacing' diff --git a/packages/mobile/src/components/verification-error-drawer/VerificationErrorDrawer.tsx b/packages/mobile/src/components/verification-error-drawer/VerificationErrorDrawer.tsx new file mode 100644 index 00000000000..1f27c3e87d6 --- /dev/null +++ b/packages/mobile/src/components/verification-error-drawer/VerificationErrorDrawer.tsx @@ -0,0 +1,31 @@ +import { Text, Flex, Button, IconError } from '@audius/harmony-native' +import { AppDrawer, useDrawerState } from 'app/components/drawer/AppDrawer' + +const MODAL_NAME = 'VerificationError' + +const messages = { + drawerTitle: 'Verification Failed', + message: 'We could not verify your account. Please try again another time.', + closeText: 'Close' +} + +export const VerificationErrorDrawer = () => { + const { onClose } = useDrawerState(MODAL_NAME) + + return ( + + + + {messages.message} + + + + + ) +} diff --git a/packages/mobile/src/components/verification-success-drawer/VerificationSuccessDrawer.tsx b/packages/mobile/src/components/verification-success-drawer/VerificationSuccessDrawer.tsx new file mode 100644 index 00000000000..b9e94d93ce5 --- /dev/null +++ b/packages/mobile/src/components/verification-success-drawer/VerificationSuccessDrawer.tsx @@ -0,0 +1,35 @@ +import { Text, Flex, Button, IconVerified } from '@audius/harmony-native' +import { AppDrawer, useDrawerState } from 'app/components/drawer/AppDrawer' + +const MODAL_NAME = 'VerificationSuccess' + +const messages = { + drawerTitle: 'Verification Submitted', + message: + 'Thank you for completing identity verification. Your request will be processed soon.', + pending: 'Pending', + closeText: 'Close' +} + +export const VerificationSuccessDrawer = () => { + const { onClose } = useDrawerState(MODAL_NAME) + + return ( + + + + + + {messages.pending} + + + + {messages.message} + + + + + ) +} diff --git a/packages/mobile/src/screens/app-screen/AppScreen.tsx b/packages/mobile/src/screens/app-screen/AppScreen.tsx index 8b861015220..b282a60ca00 100644 --- a/packages/mobile/src/screens/app-screen/AppScreen.tsx +++ b/packages/mobile/src/screens/app-screen/AppScreen.tsx @@ -15,6 +15,7 @@ import { ExternalWalletsModalScreen } from '../external-wallets' import { FeatureFlagOverrideScreen } from '../feature-flag-override-screen' import { TipArtistModalScreen } from '../tip-artist-screen' import { UploadModalScreen } from '../upload-screen' +import { VerificationWebViewModalScreen } from '../verification-webview-screen' import { AppTabsScreen } from './AppTabsScreen' @@ -72,6 +73,10 @@ export const AppScreen = () => { name='ChangePassword' component={ChangePasswordModalScreen} /> + ) diff --git a/packages/mobile/src/screens/app-screen/AppTabScreen.tsx b/packages/mobile/src/screens/app-screen/AppTabScreen.tsx index 4aa82fcba17..2b936fa6eeb 100644 --- a/packages/mobile/src/screens/app-screen/AppTabScreen.tsx +++ b/packages/mobile/src/screens/app-screen/AppTabScreen.tsx @@ -110,6 +110,7 @@ export type AppTabScreenParamList = { AccountSettingsScreen: undefined ChangeEmail: undefined ChangePassword: undefined + VerificationWebView: undefined InboxSettingsScreen: undefined CommentSettingsScreen: undefined DownloadSettingsScreen: undefined diff --git a/packages/mobile/src/screens/settings-screen/AccountSettingsScreen.tsx b/packages/mobile/src/screens/settings-screen/AccountSettingsScreen.tsx index cedf709f1ab..42a881c3801 100644 --- a/packages/mobile/src/screens/settings-screen/AccountSettingsScreen.tsx +++ b/packages/mobile/src/screens/settings-screen/AccountSettingsScreen.tsx @@ -18,7 +18,8 @@ import { IconRecoveryEmail, IconSignOut, IconSkull, - IconUser + IconUser, + IconVerified } from '@audius/harmony-native' import { ScrollView, @@ -45,6 +46,10 @@ const messages = { recoveryButtonTitle: 'Resend Recovery Email', recoveryEmailSent: 'Recovery Email Sent!', recoveryEmailNotSent: 'Unable to send recovery email. Please try again!', + verifyTitle: 'Verification', + verifyDescription: + 'Verify your Audius profile by completing identity verification.', + verifyButtonTitle: 'Get Verified', emailTitle: 'Change Email', emailDescription: 'Change the email you use to sign in and receive emails.', emailButtonTitle: 'Change Email', @@ -109,6 +114,10 @@ export const AccountSettingsScreen = () => { navigation.push('ChangePassword') }, [navigation]) + const handlePressVerification = useCallback(() => { + navigation.push('VerificationWebView') + }, [navigation]) + const openSignOutDrawer = useCallback(() => { dispatch(setVisibility({ modal: 'SignOutConfirmation', visible: true })) }, [dispatch]) @@ -145,6 +154,13 @@ export const AccountSettingsScreen = () => { buttonTitle={messages.recoveryButtonTitle} onPress={handlePressRecoveryEmail} /> + { variant='title' strength='weak' textAlign='center' - color='white' + color='inverse' style={{ justifyContent: 'flex-end' }} > {messages.newToAudius}{' '} - + {messages.createAccount} diff --git a/packages/mobile/src/screens/verification-webview-screen/VerificationWebViewScreen.tsx b/packages/mobile/src/screens/verification-webview-screen/VerificationWebViewScreen.tsx new file mode 100644 index 00000000000..88fef23a023 --- /dev/null +++ b/packages/mobile/src/screens/verification-webview-screen/VerificationWebViewScreen.tsx @@ -0,0 +1,160 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +import { useQueryContext } from '@audius/common/api' +import { AuthHeaders } from '@audius/common/services' +import { modalsActions } from '@audius/common/store' +import { useNavigation } from '@react-navigation/native' +import { View } from 'react-native' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { WebView } from 'react-native-webview' +import { useDispatch } from 'react-redux' + +import { Flex, LoadingSpinner } from '@audius/harmony-native' +import { env } from 'app/services/env' +import { makeStyles } from 'app/styles' + +import { ModalScreen, Screen, ScreenContent } from '../../components/core' + +const useStyles = makeStyles(({ palette }) => ({ + loadingContainer: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: palette.background + }, + webViewContainer: { + flex: 1, + backgroundColor: palette.background + } +})) + +const { setVisibility } = modalsActions + +type VerificationResult = { + type: 'success' | 'error' | 'close' +} + +const VerificationWebViewScreen = () => { + const styles = useStyles() + const insets = useSafeAreaInsets() + const navigation = useNavigation() + const dispatch = useDispatch() + const { identityService } = useQueryContext() + const [authHeaders, setAuthHeaders] = useState<{ + [AuthHeaders.Message]: string + [AuthHeaders.Signature]: string + } | null>(null) + const webViewRef = useRef(null) + + // Fetch auth headers + useEffect(() => { + const fetchAuthHeaders = async () => { + try { + const headers = await identityService.getAuthHeaders() + setAuthHeaders(headers) + } catch (error) { + console.error('Failed to fetch auth headers:', error) + } + } + fetchAuthHeaders() + }, [identityService]) + + const handleMessage = useCallback( + (event: { nativeEvent: { data: string } }) => { + try { + const result: VerificationResult = JSON.parse(event.nativeEvent.data) + if (result.type === 'success') { + if (webViewRef.current) { + webViewRef.current.stopLoading() + } + setTimeout(() => { + navigation.goBack() + dispatch( + setVisibility({ modal: 'VerificationSuccess', visible: true }) + ) + }, 100) + } else if (result.type === 'error') { + if (webViewRef.current) { + webViewRef.current.stopLoading() + } + setTimeout(() => { + navigation.goBack() + dispatch( + setVisibility({ modal: 'VerificationError', visible: true }) + ) + }, 100) + } else if (result.type === 'close') { + if (webViewRef.current) { + webViewRef.current.stopLoading() + } + setTimeout(() => { + navigation.goBack() + }, 100) + } + } catch (error) { + console.error('Failed to parse message from WebView:', error) + } + }, + [navigation, dispatch] + ) + + const injectedJavaScript = authHeaders + ? ` + (function() { + // Store auth headers in localStorage for the web app to use + localStorage.setItem('${AuthHeaders.Message}', '${authHeaders[AuthHeaders.Message]}'); + localStorage.setItem('${AuthHeaders.Signature}', '${authHeaders[AuthHeaders.Signature]}'); + })(); + true; + ` + : '' + + const checkPageUrl = `${env.AUDIUS_URL}/check` + + if (!authHeaders) { + return ( + + + + + + + + ) + } + + return ( + + + + ( + + + + )} + /> + + + + ) +} + +export const VerificationWebViewModalScreen = () => { + return ( + + + + ) +} diff --git a/packages/mobile/src/screens/verification-webview-screen/index.ts b/packages/mobile/src/screens/verification-webview-screen/index.ts new file mode 100644 index 00000000000..65aaad36769 --- /dev/null +++ b/packages/mobile/src/screens/verification-webview-screen/index.ts @@ -0,0 +1 @@ +export * from './VerificationWebViewScreen' diff --git a/packages/web/src/components/app-redirect-popover/components/AppRedirectPopover.tsx b/packages/web/src/components/app-redirect-popover/components/AppRedirectPopover.tsx index 6976393ddd3..eedb1a91752 100644 --- a/packages/web/src/components/app-redirect-popover/components/AppRedirectPopover.tsx +++ b/packages/web/src/components/app-redirect-popover/components/AppRedirectPopover.tsx @@ -14,6 +14,14 @@ import { getPathname } from 'utils/route' import styles from './AppRedirectPopover.module.css' const animatedAny = animated as any +declare global { + interface Window { + ReactNativeWebView?: { + postMessage: (message: string) => void + } + } +} + const { APP_REDIRECT, SIGN_UP_PAGE } = route const messages = { @@ -102,6 +110,11 @@ export const AppRedirectPopover = (props: AppRedirectPopoverProps) => { 'app-redirect-popover', false ) + const [isInWebView, setIsInWebView] = useState(false) + + useEffect(() => { + setIsInWebView(Boolean(window.ReactNativeWebView?.postMessage)) + }, []) const [animDelay, setAnimDelay] = useState(false) useEffect(() => { @@ -112,7 +125,8 @@ export const AppRedirectPopover = (props: AppRedirectPopoverProps) => { !(matchPath('/', location.pathname)?.pathname === location.pathname) && animDelay && !isDismissed && - isMobile + isMobile && + !isInWebView useEffect(() => { shouldShow && incrementScroll() diff --git a/packages/web/src/components/bottom-bar/BottomBar.tsx b/packages/web/src/components/bottom-bar/BottomBar.tsx index a4b875b8150..72411d24e89 100644 --- a/packages/web/src/components/bottom-bar/BottomBar.tsx +++ b/packages/web/src/components/bottom-bar/BottomBar.tsx @@ -12,6 +12,14 @@ import TrendingButton from 'components/bottom-bar/buttons/TrendingButton' import styles from './BottomBar.module.css' +declare global { + interface Window { + ReactNativeWebView?: { + postMessage: (message: string) => void + } + } +} + const { FEED_PAGE, TRENDING_PAGE, EXPLORE_PAGE, FAVORITES_PAGE, LIBRARY_PAGE } = route @@ -63,7 +71,7 @@ const BottomBar = ({ [setStackReset, pathname] ) - return ( + return window.ReactNativeWebView?.postMessage ? null : (
void + } + } +} + interface OwnProps { className?: string } @@ -20,6 +29,14 @@ const Navigator = ({ className }: OwnProps) => { const isElectron = client === Client.ELECTRON + // Hide navigation when in a React Native WebView (e.g., mobile app WebView) + const isInWebView = + typeof window !== 'undefined' && window.ReactNativeWebView !== undefined + + if (isInWebView) { + return null + } + return (
{ css={{ minWidth: 'max-content' }} > {messages.launchYourOwn} - - - {messages.required} - + - + + {messages.required} + + + + - + diff --git a/packages/web/src/pages/artist-coins-launchpad-page/pages/SplashPage.tsx b/packages/web/src/pages/artist-coins-launchpad-page/pages/SplashPage.tsx index 7044df29f10..38bebe04637 100644 --- a/packages/web/src/pages/artist-coins-launchpad-page/pages/SplashPage.tsx +++ b/packages/web/src/pages/artist-coins-launchpad-page/pages/SplashPage.tsx @@ -35,7 +35,8 @@ const messages = { launchPanelDescription2: 'It only takes a few steps to set things up and share it with your fans.', launchPanelButtonText: 'Get Started!', - verifiedOnlyTooltip: 'Verified users only' + verifiedOnlyTooltip: + 'Verified users only. Request account verification in Settings.' } const features = [ diff --git a/packages/web/src/pages/check-page/CheckPage.tsx b/packages/web/src/pages/check-page/CheckPage.tsx index 66514840de3..cf336033e3d 100644 --- a/packages/web/src/pages/check-page/CheckPage.tsx +++ b/packages/web/src/pages/check-page/CheckPage.tsx @@ -1,10 +1,12 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useAccountStatus, useCurrentAccountUser } from '@audius/common/api' import { Status } from '@audius/common/models' +import { AuthHeaders } from '@audius/common/services' import { route } from '@audius/common/utils' -import { usePlaidLink } from 'react-plaid-link' +import { usePlaidLink, PlaidLinkError } from 'react-plaid-link' import { useDispatch } from 'react-redux' +import { useNavigate } from 'react-router' import Page from 'components/page/Page' import { identityService } from 'services/audius-sdk/identity' @@ -12,22 +14,38 @@ import { push as pushRoute } from 'utils/navigation' import './CheckPage.module.css' -const { SIGN_IN_PAGE, TRENDING_PAGE } = route +declare global { + interface Window { + ReactNativeWebView?: { + postMessage: (message: string) => void + } + } +} + +const { SIGN_IN_PAGE, SETTINGS_PAGE } = route const CheckPage = () => { const dispatch = useDispatch() + const navigate = useNavigate() const { data: accountHandle } = useCurrentAccountUser({ select: (user) => user?.handle }) const { data: accountStatus } = useAccountStatus() useEffect(() => { - if (accountStatus !== Status.LOADING && !accountHandle) { + const hasAuthHeaders = + typeof window !== 'undefined' && + window.localStorage && + window.localStorage.getItem(AuthHeaders.Message) !== null && + window.localStorage.getItem(AuthHeaders.Signature) !== null + + if (accountStatus !== Status.LOADING && !accountHandle && !hasAuthHeaders) { dispatch(pushRoute(SIGN_IN_PAGE)) } }, [accountHandle, accountStatus, dispatch]) const [linkToken, setLinkToken] = useState(null) + const wasSuccessful = useRef(false) useEffect(() => { async function fetchLinkToken() { @@ -37,13 +55,65 @@ const CheckPage = () => { fetchLinkToken() }, []) + const isInWebView = useRef( + typeof window !== 'undefined' && window.ReactNativeWebView !== undefined + ) + + const sendMessageToWebView = useCallback((type: 'success' | 'error') => { + if (isInWebView.current && window.ReactNativeWebView) { + window.ReactNativeWebView.postMessage(JSON.stringify({ type })) + } + }, []) + const onSuccess = useCallback(() => { - dispatch(pushRoute(TRENDING_PAGE)) - }, [dispatch]) + wasSuccessful.current = true + if (isInWebView.current) { + // In WebView, send message instead of navigating + setTimeout(() => { + sendMessageToWebView('success') + }, 500) + } else { + // In web, navigate normally + setTimeout(() => { + navigate(`${SETTINGS_PAGE}?verification=success`) + }, 500) + } + }, [navigate, sendMessageToWebView]) + + const onExit = useCallback( + (err: PlaidLinkError | null) => { + if (isInWebView.current) { + // In WebView, send message instead of navigating + if (err) { + sendMessageToWebView('error') + } else if (wasSuccessful.current) { + sendMessageToWebView('success') + } else { + // User exited without completing - just close + if (window.ReactNativeWebView) { + window.ReactNativeWebView.postMessage( + JSON.stringify({ type: 'close' }) + ) + } + } + } else { + // In web, navigate normally + if (err) { + navigate(`${SETTINGS_PAGE}?verification=error`) + } else if (wasSuccessful.current) { + navigate(`${SETTINGS_PAGE}?verification=success`) + } else { + navigate(SETTINGS_PAGE) + } + } + }, + [navigate, sendMessageToWebView] + ) const { open, ready } = usePlaidLink({ token: linkToken, - onSuccess + onSuccess, + onExit }) useEffect(() => { diff --git a/packages/web/src/pages/coin-detail-page/components/OpenAppDrawer.tsx b/packages/web/src/pages/coin-detail-page/components/OpenAppDrawer.tsx index 8f3dfa033f5..06eb022fa05 100644 --- a/packages/web/src/pages/coin-detail-page/components/OpenAppDrawer.tsx +++ b/packages/web/src/pages/coin-detail-page/components/OpenAppDrawer.tsx @@ -19,6 +19,8 @@ const messages = { "You'll need to make this purchase in the app or on the web." } +const isInWebView = Boolean(window.ReactNativeWebView?.postMessage) + export const OpenAppDrawer = ({ isOpen, onClose }: OpenAppDrawerProps) => { const location = useLocation() @@ -28,7 +30,7 @@ export const OpenAppDrawer = ({ isOpen, onClose }: OpenAppDrawerProps) => { window.location.href = redirectHref }, [location]) - return ( + return isInWebView ? null : ( { return ( <> } + icon={} title={messages.title} description={messages.description} > diff --git a/packages/web/src/pages/settings-page/components/desktop/DeveloperApps/DeveloperAppsSettingsCard.tsx b/packages/web/src/pages/settings-page/components/desktop/DeveloperApps/DeveloperAppsSettingsCard.tsx index 99d0b343416..0a3f4093be6 100644 --- a/packages/web/src/pages/settings-page/components/desktop/DeveloperApps/DeveloperAppsSettingsCard.tsx +++ b/packages/web/src/pages/settings-page/components/desktop/DeveloperApps/DeveloperAppsSettingsCard.tsx @@ -26,7 +26,7 @@ export const DeveloperAppsSettingsCard = () => { return ( <> } + icon={} title={messages.title} description={messages.description} > diff --git a/packages/web/src/pages/settings-page/components/desktop/LabelAccount/LabelAccountSettingsCard.tsx b/packages/web/src/pages/settings-page/components/desktop/LabelAccount/LabelAccountSettingsCard.tsx index 34544b9714b..e320ee47464 100644 --- a/packages/web/src/pages/settings-page/components/desktop/LabelAccount/LabelAccountSettingsCard.tsx +++ b/packages/web/src/pages/settings-page/components/desktop/LabelAccount/LabelAccountSettingsCard.tsx @@ -28,7 +28,7 @@ export const LabelAccountSettingsCard = () => { return ( <> } + icon={} title={settingsMessages.labelAccountCardTitle} description={settingsMessages.labelAccountCardDescription} > diff --git a/packages/web/src/pages/settings-page/components/desktop/ListeningHistory/ListeningHistorySettingsCard.tsx b/packages/web/src/pages/settings-page/components/desktop/ListeningHistory/ListeningHistorySettingsCard.tsx index e76dd95ea4c..8689ef65c76 100644 --- a/packages/web/src/pages/settings-page/components/desktop/ListeningHistory/ListeningHistorySettingsCard.tsx +++ b/packages/web/src/pages/settings-page/components/desktop/ListeningHistory/ListeningHistorySettingsCard.tsx @@ -25,7 +25,7 @@ export const ListeningHistorySettingsCard = () => { return ( } + icon={} title={messages.title} description={messages.description} > diff --git a/packages/web/src/pages/settings-page/components/desktop/ManagerMode/AccountsManagingYouSettingsCard.tsx b/packages/web/src/pages/settings-page/components/desktop/ManagerMode/AccountsManagingYouSettingsCard.tsx index 22773f96f22..be4ec3de64c 100644 --- a/packages/web/src/pages/settings-page/components/desktop/ManagerMode/AccountsManagingYouSettingsCard.tsx +++ b/packages/web/src/pages/settings-page/components/desktop/ManagerMode/AccountsManagingYouSettingsCard.tsx @@ -41,7 +41,7 @@ export const AccountsManagingYouSettingsCard = () => { return ( <> } + icon={} title={messages.accountsManagingYouTitle} description={messages.accountsManagingYouDescription} > diff --git a/packages/web/src/pages/settings-page/components/desktop/ManagerMode/AccountsYouManageSettingsCard.tsx b/packages/web/src/pages/settings-page/components/desktop/ManagerMode/AccountsYouManageSettingsCard.tsx index d3e81f32420..0ba1c61b97b 100644 --- a/packages/web/src/pages/settings-page/components/desktop/ManagerMode/AccountsYouManageSettingsCard.tsx +++ b/packages/web/src/pages/settings-page/components/desktop/ManagerMode/AccountsYouManageSettingsCard.tsx @@ -45,7 +45,7 @@ export const AccountsYouManageSettingsCard = () => { return ( <> } + icon={} title={messages.accountsYouManageTitle} description={messages.accountsYouManageDescription} > diff --git a/packages/web/src/pages/settings-page/components/desktop/PayoutWallet/PayoutWalletSettingsCard.tsx b/packages/web/src/pages/settings-page/components/desktop/PayoutWallet/PayoutWalletSettingsCard.tsx index acf3d39987d..a8e363a9bfa 100644 --- a/packages/web/src/pages/settings-page/components/desktop/PayoutWallet/PayoutWalletSettingsCard.tsx +++ b/packages/web/src/pages/settings-page/components/desktop/PayoutWallet/PayoutWalletSettingsCard.tsx @@ -23,7 +23,7 @@ export const PayoutWalletSettingsCard = () => { return ( <> } + icon={} title={messages.title} description={messages.description} > diff --git a/packages/web/src/pages/settings-page/components/desktop/SettingsCard.module.css b/packages/web/src/pages/settings-page/components/desktop/SettingsCard.module.css index 89beea75e55..55c9b56032c 100644 --- a/packages/web/src/pages/settings-page/components/desktop/SettingsCard.module.css +++ b/packages/web/src/pages/settings-page/components/desktop/SettingsCard.module.css @@ -1,5 +1,6 @@ .settingsCard { - flex: 1 1 calc(50% - var(--harmony-unit-3)); /* Leave room for gap */ + flex: 1 1 calc(50% - var(--harmony-unit-3)); + /* Leave room for gap */ min-width: 340px; border-radius: 6px; background-color: var(--harmony-white); @@ -15,7 +16,8 @@ } .settingsCardFull { - flex: 1 1 calc(100% - var(--harmony-unit-3)); /* Leave room for gap */ + flex: 1 1 calc(100% - var(--harmony-unit-3)); + /* Leave room for gap */ } .title { @@ -35,10 +37,6 @@ display: block; } -.title path { - fill: var(--harmony-secondary); -} - .description { color: var(--harmony-neutral); font-family: var(--harmony-font-famliy); diff --git a/packages/web/src/pages/settings-page/components/desktop/SettingsPage.tsx b/packages/web/src/pages/settings-page/components/desktop/SettingsPage.tsx index bd60d7e2d74..6e6442ae097 100644 --- a/packages/web/src/pages/settings-page/components/desktop/SettingsPage.tsx +++ b/packages/web/src/pages/settings-page/components/desktop/SettingsPage.tsx @@ -20,8 +20,10 @@ import { import { route } from '@audius/common/utils' import { Button, + Flex, IconAppearance, IconEmailAddress, + IconError, IconKey, IconRecoveryEmail as IconMail, IconMessage, @@ -30,6 +32,7 @@ import { IconReceive, IconSettings, IconSignOut, + IconVerified, Modal, ModalContent, ModalContentText, @@ -37,11 +40,12 @@ import { ModalHeader, ModalTitle, SegmentedControl, + Text, useTheme } from '@audius/harmony' import cn from 'classnames' import { useDispatch } from 'react-redux' -import { Link } from 'react-router' +import { Link, useSearchParams } from 'react-router' import { useModalState } from 'common/hooks/useModalState' import { make, useRecord } from 'common/store/analytics/actions' @@ -60,6 +64,7 @@ import { Permission } from 'utils/browserNotifications' import { isElectron } from 'utils/clientUtil' +import { push } from 'utils/navigation' import { useSelector } from 'utils/reducer' import { THEME_KEY } from 'utils/theme/theme' @@ -95,6 +100,7 @@ const { const { subscribeBrowserPushNotifications } = accountActions const { + CHECK_PAGE, DOWNLOAD_LINK, PRIVACY_POLICY, PRIVATE_KEY_EXPORTER_SETTINGS_PAGE, @@ -106,7 +112,15 @@ const EMAIL_TOAST_TIMEOUT = 2000 const messages = { title: 'Settings', - description: 'Configure your Audius account' + description: 'Configure your Audius account', + verificationSuccessTitle: 'Verification Submitted', + verificationSuccessMessage: + 'Thank you for completing identity verification. Your request will be processed soon.', + verificationErrorTitle: 'Verification Failed', + verificationErrorMessage: + 'We could not verify your account. Please try again another time.', + pending: 'Pending', + closeButton: 'Close' } export const SettingsPage = () => { @@ -114,14 +128,16 @@ export const SettingsPage = () => { const isManagedAccount = useIsManagedAccount() const { authService, identityService } = useQueryContext() const { spacing } = useTheme() + const [searchParams, setSearchParams] = useSearchParams() const { data: accountData } = useCurrentAccountUser({ select: (user) => ({ handle: user?.handle, - userId: user?.user_id + userId: user?.user_id, + isVerified: user?.is_verified }) }) - const { handle, userId } = accountData ?? {} + const { handle, userId, isVerified } = accountData ?? {} const theme = useSelector(getTheme) const emailFrequency = useSelector(getEmailFrequency) const notificationSettings = useSelector(getBrowserNotificationSettings) @@ -147,6 +163,33 @@ export const SettingsPage = () => { const [, setIsInboxSettingsModalVisible] = useModalState('InboxSettings') const [, setIsCommentSettingsModalVisible] = useModalState('CommentSettings') + // Check for verification query param and show appropriate modal + const verificationStatus = searchParams.get('verification') + const [isVerificationSuccessModalOpen, setIsVerificationSuccessModalOpen] = + useState(false) + const [isVerificationErrorModalOpen, setIsVerificationErrorModalOpen] = + useState(false) + + useEffect(() => { + if (verificationStatus === 'success') { + setIsVerificationSuccessModalOpen(true) + searchParams.delete('verification') + setSearchParams(searchParams, { replace: true }) + } else if (verificationStatus === 'error') { + setIsVerificationErrorModalOpen(true) + searchParams.delete('verification') + setSearchParams(searchParams, { replace: true }) + } + }, [verificationStatus, searchParams, setSearchParams]) + + const handleCloseVerificationSuccessModal = useCallback(() => { + setIsVerificationSuccessModalOpen(false) + }, []) + + const handleCloseVerificationErrorModal = useCallback(() => { + setIsVerificationErrorModalOpen(false) + }, []) + useEffect(() => { dispatch(getNotificationSettings()) }, [dispatch]) @@ -246,6 +289,10 @@ export const SettingsPage = () => { record(make(Name.EXPORT_PRIVATE_KEY_LINK_CLICKED, { handle, userId })) }, [record, handle, userId]) + const goToVerification = useCallback(() => { + dispatch(push(CHECK_PAGE)) + }, [dispatch]) + const toggleBrowserPushNotificationPermissions = useCallback( (notificationType: BrowserNotificationSetting, isOn: boolean) => { if (!isOn) { @@ -341,7 +388,7 @@ export const SettingsPage = () => {
{!isManagedAccount ? ( } + icon={} title={settingsMessages.appearanceTitle} description={settingsMessages.appearanceDescription} isFull={true} @@ -358,7 +405,7 @@ export const SettingsPage = () => { ) : null} {!isManagedAccount ? ( } + icon={} title={settingsMessages.inboxSettingsCardTitle} description={settingsMessages.inboxSettingsCardDescription} > @@ -372,7 +419,7 @@ export const SettingsPage = () => { ) : null} } + icon={} title={settingsMessages.commentSettingsCardTitle} description={settingsMessages.commentSettingsCardDescription} > @@ -385,7 +432,7 @@ export const SettingsPage = () => { } + icon={} title={settingsMessages.notificationsCardTitle} description={settingsMessages.notificationsCardDescription} > @@ -399,7 +446,7 @@ export const SettingsPage = () => { {!isManagedAccount ? ( } + icon={} title={settingsMessages.accountRecoveryCardTitle} description={settingsMessages.accountRecoveryCardDescription} > @@ -418,7 +465,23 @@ export const SettingsPage = () => { ) : null} {!isManagedAccount ? ( } + icon={} + title={settingsMessages.verificationCardTitle} + description={settingsMessages.verificationCardDescription} + > + + + ) : null} + {!isManagedAccount ? ( + } title={settingsMessages.changeEmailCardTitle} description={settingsMessages.changeEmailCardDescription} > @@ -433,7 +496,7 @@ export const SettingsPage = () => { ) : null} {!isManagedAccount ? ( } + icon={} title={settingsMessages.changePasswordCardTitle} description={settingsMessages.changePasswordCardDescription} > @@ -451,7 +514,7 @@ export const SettingsPage = () => { {isDownloadDesktopEnabled ? ( } + icon={} title={settingsMessages.desktopAppCardTitle} description={settingsMessages.desktopAppCardDescription} > @@ -576,6 +639,81 @@ export const SettingsPage = () => { emailFrequency={emailFrequency} onClose={closeNotificationSettings} /> + + + + + + + + + + {messages.pending} + + + + + {messages.verificationSuccessMessage} + + + + + + + + + + + + + + + + + {messages.verificationErrorMessage} + + + + + + + + ) } diff --git a/packages/web/src/pages/settings-page/components/desktop/WormholeConversionSettingsCard.tsx b/packages/web/src/pages/settings-page/components/desktop/WormholeConversionSettingsCard.tsx index 42e800b97a5..5709a26e2f1 100644 --- a/packages/web/src/pages/settings-page/components/desktop/WormholeConversionSettingsCard.tsx +++ b/packages/web/src/pages/settings-page/components/desktop/WormholeConversionSettingsCard.tsx @@ -113,7 +113,7 @@ export const WormholeConversionSettingsCard = () => { return ( } + icon={} title={messages.title} description={messages.description} > diff --git a/packages/web/src/pages/settings-page/components/mobile/AccountSettingsPage.tsx b/packages/web/src/pages/settings-page/components/mobile/AccountSettingsPage.tsx index 88e5fb248e1..12d4f617459 100644 --- a/packages/web/src/pages/settings-page/components/mobile/AccountSettingsPage.tsx +++ b/packages/web/src/pages/settings-page/components/mobile/AccountSettingsPage.tsx @@ -9,6 +9,7 @@ import { IconEmailAddress, IconKey, IconSignOut, + IconVerified, Flex, Text, IconComponent, @@ -28,7 +29,11 @@ import { push } from 'utils/navigation' import styles from './AccountSettingsPage.module.css' import settingsPageStyles from './SettingsPage.module.css' -const { CHANGE_EMAIL_SETTINGS_PAGE, CHANGE_PASSWORD_SETTINGS_PAGE } = route +const { + CHECK_PAGE, + CHANGE_EMAIL_SETTINGS_PAGE, + CHANGE_PASSWORD_SETTINGS_PAGE +} = route const messages = { title: 'Account', @@ -39,6 +44,10 @@ const messages = { recoveryButtonTitle: 'Resend Recovery Email', recoveryEmailSent: 'Recovery Email Sent!', recoveryEmailNotSent: 'Unable to send recovery email. Please try again!', + verifyTitle: 'Verify Your Account', + verifyDescription: + 'Verify your Audius profile by completing identity verification', + verifyButtonTitle: 'Get Verified', emailTitle: 'Change Email', emailDescription: 'Change the email you use to sign in and receive emails.', emailButtonTitle: 'Change Email', @@ -166,6 +175,10 @@ const AccountSettingsPage = () => { goToRoute(CHANGE_PASSWORD_SETTINGS_PAGE) }, [goToRoute]) + const goToVerification = useCallback(() => { + goToRoute(CHECK_PAGE) + }, [goToRoute]) + const goToChangeEmailSettingsPage = useCallback(() => { goToRoute(CHANGE_EMAIL_SETTINGS_PAGE) }, [goToRoute]) @@ -194,6 +207,13 @@ const AccountSettingsPage = () => { buttonTitle={messages.recoveryButtonTitle} onClick={onClickRecover} /> + { const { subPage } = props const dispatch = useDispatch() useScrollToTop() + const [searchParams, setSearchParams] = useSearchParams() const { data: accountData } = useCurrentAccountUser({ select: (user) => ({ @@ -103,6 +119,35 @@ export const SettingsPage = (props: SettingsPageProps) => { const { tier } = useTierAndVerifiedForUser(userId) const showMatrix = tier === 'gold' || tier === 'platinum' + // Check for verification query param and show appropriate modal + const verificationStatus = searchParams.get('verification') + const [isVerificationSuccessModalOpen, setIsVerificationSuccessModalOpen] = + useState(false) + const [isVerificationErrorModalOpen, setIsVerificationErrorModalOpen] = + useState(false) + + useEffect(() => { + if (verificationStatus === 'success') { + setIsVerificationSuccessModalOpen(true) + // Remove query param from URL + searchParams.delete('verification') + setSearchParams(searchParams, { replace: true }) + } else if (verificationStatus === 'error') { + setIsVerificationErrorModalOpen(true) + // Remove query param from URL + searchParams.delete('verification') + setSearchParams(searchParams, { replace: true }) + } + }, [verificationStatus, searchParams, setSearchParams]) + + const handleCloseVerificationSuccessModal = useCallback(() => { + setIsVerificationSuccessModalOpen(false) + }, []) + + const handleCloseVerificationErrorModal = useCallback(() => { + setIsVerificationErrorModalOpen(false) + }, []) + useEffect(() => { dispatch(getPushNotificationSettingsAction()) }, [dispatch]) @@ -237,6 +282,72 @@ export const SettingsPage = (props: SettingsPageProps) => {
+ + + + + + + {messages.pending} + + + + + {messages.verificationSuccessMessage} + + + + + + + + + + + + + + {messages.verificationErrorMessage} + + + + + + + + ) }