Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[PAY-1934] Fix some issues with purchase modal state #6231

Merged
merged 1 commit into from
Oct 4, 2023
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
11 changes: 9 additions & 2 deletions packages/mobile/src/components/drawer/NativeDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { DrawerProps } from './Drawer'
import Drawer from './Drawer'

type NativeDrawerProps = SetOptional<DrawerProps, 'isOpen' | 'onClose'> & {
blockClose?: boolean
drawerName: DrawerName
}

Expand All @@ -17,14 +18,20 @@ type NativeDrawerProps = SetOptional<DrawerProps, 'isOpen' | 'onClose'> & {
* opening and closing.
*/
export const NativeDrawer = (props: NativeDrawerProps) => {
const { drawerName, onClose: onCloseProp, ...other } = props
const {
blockClose = true,
drawerName,
onClose: onCloseProp,
...other
} = props

const { isOpen, onClose, onClosed, visibleState } = useDrawer(drawerName)

const handleClose = useCallback(() => {
if (blockClose) return
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: prefer non-early return here imo

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh interesting I usually feel the other way! What's the reasoning behind wrapping it all in a conditional?

Copy link
Contributor

Choose a reason for hiding this comment

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

at a glance you know the two other methods are fired conditionally, vs having to have the whole fn context. I go either way based on the context - here I think the method is short enough that it makes sense to just use the conditional block. I tend to use early returns in arg validation scenarios and when the block is super large

onCloseProp?.()
onClose()
}, [onCloseProp, onClose])
}, [blockClose, onCloseProp, onClose])

if (visibleState === false) return null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import type { PurchasableTrackMetadata } from '@audius/common'
import {
PurchaseContentStage,
formatPrice,
isContentPurchaseInProgress,
isTrackPurchasable,
payExtraAmountPresetValues,
purchaseContentActions,
purchaseContentSelectors,
statusIsNotFinalized,
useGetTrackById,
usePurchaseContentFormConfiguration
} from '@audius/common'
import { Formik, useFormikContext } from 'formik'
import { Linking, View, ScrollView, TouchableOpacity } from 'react-native'
import { useDispatch } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import { toFormikValidationSchema } from 'zod-formik-adapter'

import IconCart from 'app/assets/images/iconCart.svg'
Expand All @@ -35,6 +37,9 @@ import { PurchaseSuccess } from './PurchaseSuccess'
import { PurchaseSummaryTable } from './PurchaseSummaryTable'
import { usePurchaseContentFormState } from './hooks/usePurchaseContentFormState'

const { getPurchaseContentFlowStage, getPurchaseContentError } =
purchaseContentSelectors

const PREMIUM_TRACK_PURCHASE_MODAL_NAME = 'PremiumTrackPurchase'

const messages = {
Expand Down Expand Up @@ -188,7 +193,9 @@ const RenderForm = ({ track }: { track: PurchasableTrackMetadata }) => {
<>
<ScrollView contentContainerStyle={styles.formContentContainer}>
<TrackDetailsTile trackId={track.track_id} />
<PayExtraFormSection amountPresets={payExtraAmountPresetValues} />
{isPurchaseSuccessful ? null : (
<PayExtraFormSection amountPresets={payExtraAmountPresetValues} />
)}
<PurchaseSummaryTable
{...purchaseSummaryValues}
isPurchaseSuccessful={isPurchaseSuccessful}
Expand All @@ -213,33 +220,37 @@ const RenderForm = ({ track }: { track: PurchasableTrackMetadata }) => {
</View>
)}
</ScrollView>
<View style={styles.formActions}>
{error ? (
<View style={styles.errorContainer}>
<IconError
fill={accentRed}
width={spacing(5)}
height={spacing(5)}
/>
<Text weight='medium' colorValue={accentRed}>
{messages.error}
</Text>
</View>
) : null}
<Button
onPress={submitForm}
disabled={isUnlocking}
title={
isUnlocking ? messages.purchasing : messages.buy(formatPrice(price))
}
variant={'primary'}
size='large'
color={specialLightGreen}
iconPosition='left'
icon={isUnlocking ? LoadingSpinner : undefined}
fullWidth
/>
</View>
{isPurchaseSuccessful ? null : (
<View style={styles.formActions}>
{error ? (
<View style={styles.errorContainer}>
<IconError
fill={accentRed}
width={spacing(5)}
height={spacing(5)}
/>
<Text weight='medium' colorValue={accentRed}>
{messages.error}
</Text>
</View>
) : null}
<Button
onPress={submitForm}
disabled={isUnlocking}
title={
isUnlocking
? messages.purchasing
: messages.buy(formatPrice(price))
}
variant={'primary'}
size='large'
color={specialLightGreen}
iconPosition='left'
icon={isUnlocking ? LoadingSpinner : undefined}
fullWidth
/>
</View>
)}
</>
)
}
Expand All @@ -254,6 +265,9 @@ export const PremiumTrackPurchaseDrawer = () => {
{ id: trackId },
{ disabled: !trackId }
)
const stage = useSelector(getPurchaseContentFlowStage)
const error = useSelector(getPurchaseContentError)
const isUnlocking = !error && isContentPurchaseInProgress(stage)

const isLoading = statusIsNotFinalized(trackStatus)

Expand All @@ -268,6 +282,7 @@ export const PremiumTrackPurchaseDrawer = () => {

return (
<NativeDrawer
blockClose={isUnlocking}
drawerHeader={PremiumTrackPurchaseDrawerHeader}
drawerName={PREMIUM_TRACK_PURCHASE_MODAL_NAME}
onClosed={handleClosed}
Expand Down
15 changes: 13 additions & 2 deletions packages/web/src/components/drawer/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type DrawerProps = {
children: ReactNode
shouldClose?: boolean
onClose?: () => void
onClosed?: () => void
isFullscreen?: boolean
}

Expand Down Expand Up @@ -319,7 +320,12 @@ const interpolateBorderRadius = (r: number) => {
return `${r2}px ${r2}px 0px 0px`
}

const FullscreenDrawer = ({ children, isOpen, onClose }: DrawerProps) => {
const FullscreenDrawer = ({
children,
isOpen,
onClose,
onClosed
}: DrawerProps) => {
const drawerRef = useRef<HTMLDivElement | null>(null)
// Lock to prevent double scrollbars
useEffect(() => {
Expand Down Expand Up @@ -348,7 +354,12 @@ const FullscreenDrawer = ({ children, isOpen, onClose }: DrawerProps) => {
y: 1,
borderRadius: 40
},
config: slowWobble
config: slowWobble,
onDestroyed: () => {
if (!isOpen && onClosed) {
onClosed()
}
}
})
return (
<Portal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import {
usePremiumContentPurchaseModal,
usePurchaseContentFormConfiguration,
buyUSDCActions,
purchaseContentActions
purchaseContentActions,
purchaseContentSelectors,
isContentPurchaseInProgress
} from '@audius/common'
import { IconCart, ModalContent, ModalFooter, ModalHeader } from '@audius/stems'
import cn from 'classnames'
import { Formik } from 'formik'
import { useDispatch } from 'react-redux'
import { Formik, useFormikContext } from 'formik'
import { useDispatch, useSelector } from 'react-redux'
import { toFormikValidationSchema } from 'zod-formik-adapter'

import { Icon } from 'components/Icon'
Expand All @@ -33,6 +35,8 @@ import { usePurchaseContentFormState } from './hooks/usePurchaseContentFormState
const { startRecoveryIfNecessary, cleanup: cleanupUSDCRecovery } =
buyUSDCActions
const { cleanup } = purchaseContentActions
const { getPurchaseContentFlowStage, getPurchaseContentError } =
purchaseContentSelectors

const messages = {
completePurchase: 'Complete Purchase'
Expand All @@ -58,16 +62,10 @@ const RenderForm = ({
const { error, isUnlocking, purchaseSummaryValues, stage } =
usePurchaseContentFormState({ price })

// Attempt recovery once on re-mount of the form
useEffect(() => {
dispatch(startRecoveryIfNecessary)
}, [dispatch])
const { resetForm } = useFormikContext()

const handleClose = useCallback(() => {
dispatch(cleanupUSDCRecovery())
onClose()
dispatch(cleanup())
}, [dispatch, onClose])
// Reset form on track change
useEffect(() => resetForm, [track.track_id, resetForm])

// Navigate to track on successful purchase behind the modal
useEffect(() => {
Expand All @@ -82,7 +80,7 @@ const RenderForm = ({
<ModalForm>
<ModalHeader
className={cn(styles.modalHeader, { [styles.mobile]: mobile })}
onClose={handleClose}
onClose={onClose}
showDismissButton={!mobile}
>
<Text
Expand Down Expand Up @@ -123,12 +121,16 @@ const RenderForm = ({
}

export const PremiumContentPurchaseModal = () => {
const dispatch = useDispatch()
const {
isOpen,
onClose,
onClosed,
data: { contentId: trackId }
} = usePremiumContentPurchaseModal()
const stage = useSelector(getPurchaseContentFlowStage)
const error = useSelector(getPurchaseContentError)
const isUnlocking = !error && isContentPurchaseInProgress(stage)

const { data: track } = useGetTrackById(
{ id: trackId! },
Expand All @@ -140,15 +142,33 @@ export const PremiumContentPurchaseModal = () => {

const isValidTrack = track && isTrackPurchasable(track)

// Attempt recovery once on re-mount of the form
useEffect(() => {
dispatch(startRecoveryIfNecessary)
}, [dispatch])

const handleClose = useCallback(() => {
// Don't allow closing if we're in the middle of a purchase
if (!isUnlocking) {
onClose()
}
}, [isUnlocking, onClose])

const handleClosed = useCallback(() => {
onClosed()
dispatch(cleanup())
dispatch(cleanupUSDCRecovery())
}, [onClosed, dispatch])

if (track && !isValidTrack) {
console.error('PremiumContentPurchaseModal: Track is not purchasable')
}

return (
<ModalDrawer
isOpen={isOpen}
onClose={onClose}
onClosed={onClosed}
onClose={handleClose}
onClosed={handleClosed}
bodyClassName={styles.modal}
isFullscreen
useGradientTitle={false}
Expand All @@ -160,7 +180,7 @@ export const PremiumContentPurchaseModal = () => {
validationSchema={toFormikValidationSchema(validationSchema)}
onSubmit={onSubmit}
>
<RenderForm track={track} onClose={onClose} />
<RenderForm track={track} onClose={handleClose} />
</Formik>
) : null}
</ModalDrawer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const ModalDrawer = (props: ModalDrawerProps) => {
<Drawer
isOpen={props.isOpen}
onClose={props.onClose}
onClosed={props.onClosed}
isFullscreen={
props.isFullscreen === undefined ? true : props.isFullscreen
}
Expand All @@ -47,6 +48,7 @@ const ModalDrawer = (props: ModalDrawerProps) => {
<Modal
isOpen={props.isOpen}
onClose={props.onClose}
onClosed={props.onClosed}
showTitleHeader={props.showTitleHeader}
showDismissButton={props.showDismissButton}
dismissOnClickOutside={props.dismissOnClickOutside}
Expand Down