feat: add user status state

This commit is contained in:
Aidar Shaikhutdin @makeweb.space 2023-06-19 11:43:09 +03:00
parent 2bc71feb3e
commit 9255c79dc6
12 changed files with 123 additions and 60 deletions

View File

@ -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
}
}

View File

@ -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

View File

@ -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()))

View File

@ -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>

View File

@ -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 }}>{

View File

@ -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(() => {

View File

@ -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 }}>{

View File

@ -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()]

View File

@ -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
View 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

View File

@ -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

View File

@ -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
},