feat: add user status state
This commit is contained in:
parent
2bc71feb3e
commit
9255c79dc6
@ -21,29 +21,31 @@ export interface AppleReceiptPayload extends AuthPayload {
|
||||
export type Payload = ChargebeeReceiptPayload | AppleReceiptPayload
|
||||
|
||||
export interface Response {
|
||||
subscription_receipt: {
|
||||
id: string
|
||||
user_id: number
|
||||
status: number
|
||||
expires_at: null | string
|
||||
requested_at: string
|
||||
created_at: string
|
||||
data: {
|
||||
input: {
|
||||
subscription_items: [
|
||||
{
|
||||
item_price_id: string
|
||||
}
|
||||
],
|
||||
payment_intent: {
|
||||
gw_token: string
|
||||
gateway_account_id: string
|
||||
subscription_receipt: SubscriptionReceipt
|
||||
}
|
||||
|
||||
export interface SubscriptionReceipt {
|
||||
id: string
|
||||
user_id: number
|
||||
status: number
|
||||
expires_at: null | string
|
||||
requested_at: string
|
||||
created_at: string
|
||||
data: {
|
||||
input: {
|
||||
subscription_items: [
|
||||
{
|
||||
item_price_id: string
|
||||
}
|
||||
},
|
||||
app_bundle_id: string
|
||||
autorenewable: boolean
|
||||
error: string
|
||||
}
|
||||
],
|
||||
payment_intent: {
|
||||
gw_token: string
|
||||
gateway_account_id: string
|
||||
}
|
||||
},
|
||||
app_bundle_id: string
|
||||
autorenewable: boolean
|
||||
error: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { useState } from 'react'
|
||||
import { Routes, Route, Navigate, Outlet, useLocation } from 'react-router-dom'
|
||||
import { useAuth } from '../../auth'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { selectors } from '../../store'
|
||||
import routes, { hasNavigation } from '../../routes'
|
||||
import BirthdayPage from '../BirthdayPage'
|
||||
import BirthtimePage from '../BirthtimePage'
|
||||
import CreateProfilePage from '../CreateProfilePage'
|
||||
@ -13,7 +16,6 @@ import NotFoundPage from '../NotFoundPage'
|
||||
import Header from '../Header'
|
||||
import Navbar from '../Navbar'
|
||||
import Footer from '../Footer'
|
||||
import routes, { hasNavigation } from '../../routes'
|
||||
import './styles.css'
|
||||
|
||||
function App(): JSX.Element {
|
||||
@ -62,9 +64,14 @@ function SkipStep(): JSX.Element {
|
||||
}
|
||||
|
||||
function MainPage(): JSX.Element {
|
||||
const { user } = useAuth()
|
||||
const page = user ? routes.client.wallpaper() : routes.client.birthday()
|
||||
return <Navigate to={page} replace={true} />
|
||||
const status = useSelector(selectors.selectStatus)
|
||||
const pageMapper = {
|
||||
'lead': routes.client.birthday(),
|
||||
'registred': routes.client.subscription(),
|
||||
'subscribed': routes.client.wallpaper(),
|
||||
'unsubscribed': routes.client.subscription(),
|
||||
}
|
||||
return <Navigate to={pageMapper[status]} replace={true} />
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
@ -49,6 +49,7 @@ function EmailEnterPage(): JSX.Element {
|
||||
})
|
||||
.then(([{ user }, { item_prices }]) => {
|
||||
dispatch(actions.user.update(user))
|
||||
dispatch(actions.status.update('registred'))
|
||||
dispatch(actions.subscriptionPlan.setAll(item_prices))
|
||||
})
|
||||
.then(() => navigate(routes.client.subscription()))
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { selectors } from '../../store'
|
||||
import { usePayment } from '../../payment'
|
||||
import { actions } from '../../store'
|
||||
import {
|
||||
ApplePayBanner,
|
||||
ApplePayButton,
|
||||
@ -15,15 +17,23 @@ import UserHeader from '../UserHeader'
|
||||
import Title from '../Title'
|
||||
import Loader from '../Loader'
|
||||
import secure from './secure.png'
|
||||
import routes from '../../routes'
|
||||
import './styles.css'
|
||||
|
||||
function PaymentPage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const { applePay } = usePayment()
|
||||
const [open, setOpen] = useState(false)
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
const isLoading = applePay === null
|
||||
const isApplePayAvailable = import.meta.env.PROD && applePay?.canMakePayments()
|
||||
const email = useSelector(selectors.selectEmail)
|
||||
const onSuccess = useCallback(() => {
|
||||
dispatch(actions.status.update('subscribed'))
|
||||
navigate(routes.client.wallpaper())
|
||||
}, [dispatch, navigate])
|
||||
const onError = useCallback((error: Error) => console.error(error), [])
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -36,13 +46,17 @@ function PaymentPage(): JSX.Element {
|
||||
<img src={secure} alt='100% Secure' />
|
||||
</div>
|
||||
<Title variant='h1' className='mb-45'>{t('choose_payment')}</Title>
|
||||
{ isApplePayAvailable ? <ApplePayButton /> : <GooglePayButton /> }
|
||||
{ isApplePayAvailable
|
||||
?
|
||||
<ApplePayButton onSuccess={onSuccess} onError={onError} />
|
||||
:
|
||||
<GooglePayButton onSuccess={onSuccess} onError={onError} /> }
|
||||
<div className='payment-divider'>{t('or').toUpperCase()}</div>
|
||||
<CardButton onClick={() => setOpen(true)} />
|
||||
<p className='payment-warining'>
|
||||
{t('will_be_charged', { strongText: <strong>{t('trial_price')}</strong> })}
|
||||
</p>
|
||||
<CardModal open={open} onClose={() => setOpen(false)} />
|
||||
<CardModal open={open} onClose={() => setOpen(false)} onSuccess={onSuccess} onError={onError} />
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
import { useCallback, useEffect, useId } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { PaymentIntent } from '@chargebee/chargebee-js-types'
|
||||
import { useApi, useApiCall, extractErrorMessage } from '../../../../api'
|
||||
import { useApi, useApiCall, extractErrorMessage, SubscriptionReceipts } from '../../../../api'
|
||||
import { usePayment, ApplePayButtonOptions } from '../../../../payment'
|
||||
import { useAuth } from '../../../../auth'
|
||||
import Loader from '../../../Loader'
|
||||
import ErrorText from '../../../ErrorText'
|
||||
import routes from '../../../../routes'
|
||||
|
||||
const currencyCode = 'USD'
|
||||
const paymentMethod = 'apple_pay'
|
||||
|
||||
export function ApplePayButton(): JSX.Element {
|
||||
interface ApplePayButtonProps {
|
||||
onSuccess: (receipt: SubscriptionReceipts.SubscriptionReceipt) => void
|
||||
onError: (error: Error) => void
|
||||
}
|
||||
|
||||
export function ApplePayButton({ onSuccess, onError }: ApplePayButtonProps): JSX.Element {
|
||||
const api = useApi()
|
||||
const navigate = useNavigate()
|
||||
const buttonId = useId()
|
||||
const { i18n } = useTranslation()
|
||||
const { token } = useAuth()
|
||||
@ -38,14 +40,13 @@ export function ApplePayButton(): JSX.Element {
|
||||
applePay?.mountPaymentButton(`#${buttonId}`, buttonOptions)
|
||||
.then(() => applePay?.handlePayment())
|
||||
.then((paymentIntent) => {
|
||||
console.log('Success payment by ApplePay', paymentIntent)
|
||||
return api.createSubscriptionReceipt({
|
||||
token, receiptData: paymentIntent.id, autorenewable: true, sandbox: true,
|
||||
})
|
||||
})
|
||||
.then(() => navigate(routes.client.wallpaper()))
|
||||
.catch(console.error)
|
||||
}, [data, applePay, buttonId, navigate, i18n.language, api, token])
|
||||
.then(({ subscription_receipt }: SubscriptionReceipts.Response) => onSuccess(subscription_receipt))
|
||||
.catch((error: Error) => onError(error))
|
||||
}, [data, applePay, buttonId, i18n.language, api, token, onSuccess, onError])
|
||||
|
||||
return isPending ? <Loader /> : (
|
||||
<div id={buttonId} style={{ height: 60 }}>{
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { useEffect, useRef, useState, ChangeEvent } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
CardCVV, CardComponent, CardExpiry, CardNumber, Provider
|
||||
} from '@chargebee/chargebee-js-react-wrapper'
|
||||
import ChargebeeComponents from '@chargebee/chargebee-js-react-wrapper/dist/components/ComponentGroup'
|
||||
import { usePayment } from '../../../../payment'
|
||||
import { useApi } from '../../../../api'
|
||||
import { useApi, SubscriptionReceipts } from '../../../../api'
|
||||
import { useAuth } from '../../../../auth'
|
||||
import Modal from '../../../Modal'
|
||||
import Title from '../../../Title'
|
||||
@ -18,11 +17,12 @@ import amex from './amex.svg'
|
||||
import diners from './diners.svg'
|
||||
import discover from './discover.svg'
|
||||
import { cardStyles } from './styles'
|
||||
import routes from '../../../../routes'
|
||||
|
||||
interface CardModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onSuccess: (receipt: SubscriptionReceipts.SubscriptionReceipt) => void
|
||||
onError: (error: Error) => void
|
||||
}
|
||||
|
||||
interface Field {
|
||||
@ -54,9 +54,8 @@ const isReady = (fields: CompletedFields) => Object.values(fields).every((comple
|
||||
|
||||
const itemPriceId = 'aura-membership-2-week-USD'
|
||||
|
||||
export function CardModal({ open, onClose }: CardModalProps): JSX.Element {
|
||||
export function CardModal({ open, onClose, onSuccess, onError }: CardModalProps): JSX.Element {
|
||||
const api = useApi()
|
||||
const navigate = useNavigate()
|
||||
const cardRef = useRef<ChargebeeComponents>(null)
|
||||
const [status, setStatus] = useState<Status>('idle')
|
||||
const [fields, setFields] = useState(initCompletedFields)
|
||||
@ -80,12 +79,14 @@ export function CardModal({ open, onClose }: CardModalProps): JSX.Element {
|
||||
.then((result: ChargebeeTokenizeResult) => {
|
||||
return api.createSubscriptionReceipt({ token, itemPriceId, gwToken: result.token })
|
||||
})
|
||||
.then(() => {
|
||||
.then(({ subscription_receipt }: SubscriptionReceipts.Response) => {
|
||||
setStatus('success')
|
||||
navigate(routes.client.wallpaper())
|
||||
console.log('Success payment by Card')
|
||||
onSuccess(subscription_receipt)
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
setStatus('error')
|
||||
onError(error)
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
import { useCallback, useEffect, useId } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { PaymentIntent } from '@chargebee/chargebee-js-types'
|
||||
import { extractErrorMessage, useApi, useApiCall } from '../../../../api'
|
||||
import { SubscriptionReceipts, extractErrorMessage, useApi, useApiCall } from '../../../../api'
|
||||
import { usePayment, GooglePayButtonOptions } from '../../../../payment'
|
||||
import { useAuth } from '../../../../auth'
|
||||
import Loader from '../../../Loader'
|
||||
import ErrorText from '../../../ErrorText'
|
||||
import routes from '../../../../routes'
|
||||
|
||||
const currencyCode = 'USD'
|
||||
const paymentMethod = 'google_pay'
|
||||
|
||||
export function GooglePayButton(): JSX.Element {
|
||||
interface GooglePayButtonProps {
|
||||
onSuccess: (receipt: SubscriptionReceipts.SubscriptionReceipt) => void
|
||||
onError: (error: Error) => void
|
||||
}
|
||||
|
||||
export function GooglePayButton({ onSuccess, onError }: GooglePayButtonProps): JSX.Element {
|
||||
const api = useApi()
|
||||
const navigate = useNavigate()
|
||||
const buttonId = useId()
|
||||
const { i18n } = useTranslation()
|
||||
const { token } = useAuth()
|
||||
@ -42,9 +44,9 @@ export function GooglePayButton(): JSX.Element {
|
||||
console.log('Success payment by GooglePay', result)
|
||||
// TODO: implement api.createSubscriptionReceipt for GooglePay
|
||||
})
|
||||
.then(() => navigate(routes.client.wallpaper()))
|
||||
.catch(console.error)
|
||||
}, [data, googlePay, buttonId, navigate, i18n.language])
|
||||
.then(() => onSuccess({} as SubscriptionReceipts.SubscriptionReceipt))
|
||||
.catch((error: Error) => onError(error))
|
||||
}, [data, googlePay, buttonId, i18n.language, onSuccess, onError])
|
||||
|
||||
return isPending ? <Loader /> : (
|
||||
<div id={buttonId} style={{ height: 60 }}>{
|
||||
|
||||
@ -33,7 +33,12 @@ const routes = {
|
||||
},
|
||||
}
|
||||
|
||||
export const entrypoints = [routes.client.root(), routes.client.birthday(), routes.client.wallpaper()]
|
||||
export const entrypoints = [
|
||||
routes.client.root(),
|
||||
routes.client.birthday(),
|
||||
routes.client.subscription(),
|
||||
routes.client.wallpaper(),
|
||||
]
|
||||
export const isEntrypoint = (path: string) => entrypoints.includes(path)
|
||||
export const isNotEntrypoint = (path: string) => !isEntrypoint(path)
|
||||
export const withNavigationRoutes = [routes.client.wallpaper()]
|
||||
|
||||
@ -3,20 +3,23 @@ import token, { actions as tokenActions, selectToken } from './token'
|
||||
import user, { actions as userActions, selectUser } from './user'
|
||||
import form, { actions as formActions, selectors as formSelectors } from './form'
|
||||
import subscriptionPlans, { actions as subscriptionPlasActions, selectPlanById } from './subscriptionPlan'
|
||||
import status, { actions as userStatusActions, selectStatus } from './status'
|
||||
import { loadStore, backupStore } from './storageHelper'
|
||||
|
||||
const preloadedState = loadStore()
|
||||
export const reducer = combineReducers({ token, user, form, subscriptionPlans })
|
||||
export const reducer = combineReducers({ token, user, form, status, subscriptionPlans })
|
||||
export const actions = {
|
||||
token: tokenActions,
|
||||
user: userActions,
|
||||
form: formActions,
|
||||
status: userStatusActions,
|
||||
subscriptionPlan: subscriptionPlasActions,
|
||||
reset: createAction('reset'),
|
||||
}
|
||||
export const selectors = {
|
||||
selectToken,
|
||||
selectUser,
|
||||
selectStatus,
|
||||
selectPlanById,
|
||||
...formSelectors,
|
||||
}
|
||||
|
||||
25
src/store/status.ts
Normal file
25
src/store/status.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { createSlice, createSelector } from '@reduxjs/toolkit'
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
type UserStatus = 'lead' | 'registred' | 'subscribed' | 'unsubscribed'
|
||||
|
||||
const initialState = 'lead' as UserStatus
|
||||
|
||||
const userStatusSlice = createSlice({
|
||||
name: 'status',
|
||||
initialState,
|
||||
reducers: {
|
||||
update(state, action: PayloadAction<UserStatus>) {
|
||||
state = action.payload
|
||||
return state
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => builder.addCase('reset', () => initialState)
|
||||
})
|
||||
|
||||
export const { actions } = userStatusSlice
|
||||
export const selectStatus = createSelector(
|
||||
(state: { status: UserStatus }) => state.status,
|
||||
(status) => status
|
||||
)
|
||||
export default userStatusSlice.reducer
|
||||
@ -1,4 +1,6 @@
|
||||
import { createSlice, createEntityAdapter, createSelector, EntityState } from '@reduxjs/toolkit'
|
||||
import {
|
||||
createSlice, createEntityAdapter, createSelector, EntityState
|
||||
} from '@reduxjs/toolkit'
|
||||
import { SubscriptionItems } from '../api'
|
||||
|
||||
type SubscriptionPlan = SubscriptionItems.ItemPrice
|
||||
|
||||
@ -8,7 +8,7 @@ const authTokenSlice = createSlice({
|
||||
name: 'token',
|
||||
initialState,
|
||||
reducers: {
|
||||
update(state, action: PayloadAction<string>) {
|
||||
update(state, action: PayloadAction<AuthToken>) {
|
||||
state = action.payload
|
||||
return state
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user