Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions packages/mobile/src/screens/sign-on-screen/SignOnStack.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'

import {
useCurrentAccountUser,
Expand All @@ -9,10 +9,11 @@ import type { NativeStackNavigationOptions } from '@react-navigation/native-stac
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import {
getFinishedPhase1,
getHandleField,
getPage,
getStartedAndFinishedSignup
} from 'common/store/pages/signon/selectors'
import { Pages } from 'common/store/pages/signon/types'
import type { Pages } from 'common/store/pages/signon/types'
import { Platform } from 'react-native'
import { useSelector } from 'react-redux'

Expand All @@ -29,6 +30,7 @@ import { SelectArtistsScreen } from './screens/SelectArtistScreen'
import { SelectGenresScreen } from './screens/SelectGenresScreen'
import { SignOnScreen } from './screens/SignOnScreen'
import type { SignOnScreenParamList } from './types'
import { getSignOnScreen } from './utils/getSignOnScreen'

const Stack = createNativeStackNavigator()
const screenOptionsOverrides = { animationTypeForReplace: 'pop' as const }
Expand Down Expand Up @@ -69,15 +71,23 @@ export const SignOnStack = (props: SignOnStackProps) => {
)

const page = useSelector(getPage)
const handle = useSelector(getHandleField)
const navigation = useNavigation<SignOnScreenParamList>()
const lastHandledPage = useRef<Pages | null>(null)

// Respond to signon saga page changes
useEffect(() => {
// This occurs when a guest account confirms email and is ready to complete their account
if (page === Pages.PASSWORD) {
navigation.navigate('CreatePassword')
}
}, [navigation, page])
// Process each page transition once. Handle changes on PickHandle should not
// advance the user to FinishProfile before they submit the handle.
if (lastHandledPage.current === page) return
lastHandledPage.current = page

const screen = getSignOnScreen({
page,
hasHandle: Boolean(handle.value)
})
if (screen) navigation.navigate(screen)
}, [handle.value, navigation, page])

return (
<ScreenOptionsContext.Provider
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { fireEvent, render, screen } from '@testing-library/react-native'

import { AccountLoadingScreen } from './AccountLoadingScreen'

const mockDispatch = jest.fn()
const mockNavigate = jest.fn()
const mockSignUpAction = { type: 'SIGN_ON/SIGN_UP' }
const mockFinishSignUpAction = { type: 'SIGN_ON/FINISH_SIGN_UP' }
let mockIsFastReferral = false
let mockSignOnState: { accountReady: boolean; status: string } = {
accountReady: false,
status: 'loading'
}

jest.mock('@audius/web/src/common/store/pages/signon/actions', () => ({
finishSignUp: () => mockFinishSignUpAction,
signUp: () => mockSignUpAction
}))

jest.mock('react-redux', () => ({
useDispatch: () => mockDispatch,
useSelector: (selector: (state: unknown) => unknown) =>
selector({ signOn: mockSignOnState })
}))

jest.mock('app/hooks/useNavigation', () => ({
useNavigation: () => ({ navigate: mockNavigate })
}))

jest.mock('../hooks/useFastReferral', () => ({
useFastReferral: () => mockIsFastReferral
}))

jest.mock('app/components/loading-spinner', () => {
const React = require('react')
const { View } = require('react-native')
return (props: Record<string, unknown>) =>
React.createElement(View, { ...props, testID: 'loading-spinner' })
})

jest.mock('../components/layout', () => {
const React = require('react')
const { Text, View } = require('react-native')
return {
Page: ({ children }: { children: React.ReactNode }) =>
React.createElement(View, null, children),
Heading: ({
heading,
description
}: {
heading: string
description: string
}) =>
React.createElement(
View,
null,
React.createElement(Text, null, heading),
React.createElement(Text, null, description)
)
}
})

jest.mock(
'@audius/harmony-native',
() => {
const React = require('react')
const { Pressable, Text, View } = require('react-native')
return {
Button: ({
children,
onPress
}: {
children: React.ReactNode
onPress: () => void
}) =>
React.createElement(
Pressable,
{ accessibilityRole: 'button', onPress },
React.createElement(Text, null, children)
),
Flex: ({ children }: { children: React.ReactNode }) =>
React.createElement(View, null, children)
}
},
{ virtual: true }
)

describe('AccountLoadingScreen', () => {
beforeEach(() => {
jest.clearAllMocks()
mockIsFastReferral = false
mockSignOnState = {
accountReady: false,
status: 'loading'
}
})

it('shows a retry action when account creation fails', () => {
mockSignOnState.status = 'failure'

render(<AccountLoadingScreen />)

expect(
screen.getByText("We Couldn't Finish Creating Your Account")
).toBeOnTheScreen()
expect(screen.queryByTestId('loading-spinner')).not.toBeOnTheScreen()

fireEvent.press(screen.getByRole('button', { name: 'Try Again' }))

expect(mockDispatch).toHaveBeenCalledWith(mockSignUpAction)
})

it('continues showing progress while account creation is pending', () => {
render(<AccountLoadingScreen />)

expect(screen.getByTestId('loading-spinner')).toBeOnTheScreen()
expect(screen.queryByText('Try Again')).not.toBeOnTheScreen()
})

it('finishes signup and navigates home when the account is ready', () => {
mockSignOnState.status = 'success'

render(<AccountLoadingScreen />)

expect(mockDispatch).toHaveBeenCalledWith(mockFinishSignUpAction)
expect(mockNavigate).toHaveBeenCalledWith('HomeStack', {
screen: 'Trending'
})
})
})
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
// This loading page shows up when the users account is still being created either due to slow creation or a fast user

import { useEffect } from 'react'
import { useCallback, useEffect } from 'react'

import { finishSignUp } from '@audius/web/src/common/store/pages/signon/actions'
import {
finishSignUp,
signUp
} from '@audius/web/src/common/store/pages/signon/actions'
import {
getAccountReady,
getStatus
} from '@audius/web/src/common/store/pages/signon/selectors'
import { EditingStatus } from '@audius/web/src/common/store/pages/signon/types'
import { useDispatch, useSelector } from 'react-redux'

import { Flex } from '@audius/harmony-native'
import { Button, Flex } from '@audius/harmony-native'
import LoadingSpinner from 'app/components/loading-spinner'
import { useNavigation } from 'app/hooks/useNavigation'

Expand All @@ -21,7 +24,10 @@ import type { SignOnScreenParamList } from '../types'
const messages = {
heading: 'Your Account is Almost Ready to Rock 🤘',
description: "We're just finishing up a few things...",
continueButton: 'Continue to Audius'
failureHeading: "We Couldn't Finish Creating Your Account",
failureDescription:
'Your login is saved. Try again to finish creating your account.',
retry: 'Try Again'
}

// The user just waits here until the account is created and before being shown the welcome modal on the trending page
Expand All @@ -36,6 +42,11 @@ export const AccountLoadingScreen = () => {
const isAccountReady = isFastReferral
? accountReady
: accountReady || accountCreationStatus === EditingStatus.SUCCESS
const didAccountCreationFail = accountCreationStatus === EditingStatus.FAILURE

const handleRetry = useCallback(() => {
dispatch(signUp())
}, [dispatch])

useEffect(() => {
if (isAccountReady) {
Expand All @@ -47,14 +58,27 @@ export const AccountLoadingScreen = () => {

return (
<Page gap='3xl' justifyContent='center' alignItems='center' pb='3xl'>
<LoadingSpinner style={{ height: 36, width: 36 }} />
{didAccountCreationFail ? null : (
<LoadingSpinner style={{ height: 36, width: 36 }} />
)}
<Flex justifyContent='center'>
<Heading
heading={messages.heading}
description={messages.description}
heading={
didAccountCreationFail ? messages.failureHeading : messages.heading
}
description={
didAccountCreationFail
? messages.failureDescription
: messages.description
}
centered
/>
</Flex>
{didAccountCreationFail ? (
<Button variant='primary' onPress={handleRetry}>
{messages.retry}
</Button>
) : null}
</Page>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Pages } from '@audius/web/src/common/store/pages/signon/types'

import { getSignOnScreen } from './getSignOnScreen'

describe('getSignOnScreen', () => {
it('routes Identity-only accounts to handle selection', () => {
expect(getSignOnScreen({ page: Pages.PROFILE, hasHandle: false })).toBe(
'PickHandle'
)
})

it('routes indexed incomplete accounts to profile completion', () => {
expect(getSignOnScreen({ page: Pages.PROFILE, hasHandle: true })).toBe(
'FinishProfile'
)
})

it('preserves the guest password resume screen', () => {
expect(getSignOnScreen({ page: Pages.PASSWORD, hasHandle: true })).toBe(
'CreatePassword'
)
})

it('ignores sign-on states without a native transition', () => {
expect(getSignOnScreen({ page: Pages.EMAIL, hasHandle: false })).toBeNull()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Pages } from '@audius/web/src/common/store/pages/signon/types'

type GetSignOnScreenParams = {
page: Pages
hasHandle: boolean
}

export type ResumableSignOnScreen =
| 'CreatePassword'
| 'PickHandle'
| 'FinishProfile'

/** Maps shared sign-on state to the equivalent native navigation screen. */
export const getSignOnScreen = ({
page,
hasHandle
}: GetSignOnScreenParams): ResumableSignOnScreen | null => {
switch (page) {
case Pages.PASSWORD:
return 'CreatePassword'
case Pages.PROFILE:
return hasHandle ? 'FinishProfile' : 'PickHandle'
default:
return null
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import {
SIGN_IN_PAGE,
SIGN_UP_FINISH_PROFILE_PAGE,
SIGN_UP_HANDLE_PAGE,
SIGN_UP_PASSWORD_PAGE
} from '@audius/common/src/utils/route'
import { route } from '@audius/common/utils'
import { describe, expect, it } from 'vitest'

import { getSignOnRoute } from './getSignOnRoute'
import { Pages } from './types'

const {
SIGN_IN_PAGE,
SIGN_UP_FINISH_PROFILE_PAGE,
SIGN_UP_HANDLE_PAGE,
SIGN_UP_PASSWORD_PAGE
} = route

describe('getSignOnRoute', () => {
it('routes Identity-only accounts to handle selection', () => {
expect(
Expand Down
10 changes: 6 additions & 4 deletions packages/web/src/common/store/pages/signon/getSignOnRoute.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
import { route } from '@audius/common/utils'

import { Pages } from './types'

const {
SIGN_IN_PAGE,
SIGN_UP_FINISH_PROFILE_PAGE,
SIGN_UP_HANDLE_PAGE,
SIGN_UP_PAGE,
SIGN_UP_PASSWORD_PAGE
} from '@audius/common/src/utils/route'

import { Pages } from './types'
} = route

type GetSignOnRouteParams = {
signIn: boolean
Expand Down
Loading