From 9255c79dc6b5d76fb620f1bee0d9e93943928731 Mon Sep 17 00:00:00 2001 From: "Aidar Shaikhutdin @makeweb.space" Date: Mon, 19 Jun 2023 11:43:09 +0300 Subject: [PATCH] feat: add user status state --- src/api/resources/UserSubscriptionReceipts.ts | 46 ++++++++++--------- src/components/App/index.tsx | 15 ++++-- src/components/EmailEnterPage/index.tsx | 1 + src/components/PaymentPage/index.tsx | 22 +++++++-- .../PaymentPage/methods/ApplePay/Button.tsx | 19 ++++---- .../PaymentPage/methods/Card/Modal.tsx | 19 ++++---- .../PaymentPage/methods/GooglePay/Button.tsx | 18 ++++---- src/routes.ts | 7 ++- src/store/index.ts | 5 +- src/store/status.ts | 25 ++++++++++ src/store/subscriptionPlan.ts | 4 +- src/store/token.ts | 2 +- 12 files changed, 123 insertions(+), 60 deletions(-) create mode 100644 src/store/status.ts diff --git a/src/api/resources/UserSubscriptionReceipts.ts b/src/api/resources/UserSubscriptionReceipts.ts index 9e9208b..74647cd 100644 --- a/src/api/resources/UserSubscriptionReceipts.ts +++ b/src/api/resources/UserSubscriptionReceipts.ts @@ -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 } } diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 3b61e02..7860ad1 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -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 + const status = useSelector(selectors.selectStatus) + const pageMapper = { + 'lead': routes.client.birthday(), + 'registred': routes.client.subscription(), + 'subscribed': routes.client.wallpaper(), + 'unsubscribed': routes.client.subscription(), + } + return } export default App diff --git a/src/components/EmailEnterPage/index.tsx b/src/components/EmailEnterPage/index.tsx index 7d07266..9366659 100644 --- a/src/components/EmailEnterPage/index.tsx +++ b/src/components/EmailEnterPage/index.tsx @@ -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())) diff --git a/src/components/PaymentPage/index.tsx b/src/components/PaymentPage/index.tsx index 51c518e..a8c3fa8 100644 --- a/src/components/PaymentPage/index.tsx +++ b/src/components/PaymentPage/index.tsx @@ -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 { 100% Secure {t('choose_payment')} - { isApplePayAvailable ? : } + { isApplePayAvailable + ? + + : + }
{t('or').toUpperCase()}
setOpen(true)} />

{t('will_be_charged', { strongText: {t('trial_price')} })}

- setOpen(false)} /> + setOpen(false)} onSuccess={onSuccess} onError={onError} /> )} diff --git a/src/components/PaymentPage/methods/ApplePay/Button.tsx b/src/components/PaymentPage/methods/ApplePay/Button.tsx index c2e9e60..8dc562a 100644 --- a/src/components/PaymentPage/methods/ApplePay/Button.tsx +++ b/src/components/PaymentPage/methods/ApplePay/Button.tsx @@ -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 ? : (
{ diff --git a/src/components/PaymentPage/methods/Card/Modal.tsx b/src/components/PaymentPage/methods/Card/Modal.tsx index afae86e..c570787 100644 --- a/src/components/PaymentPage/methods/Card/Modal.tsx +++ b/src/components/PaymentPage/methods/Card/Modal.tsx @@ -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(null) const [status, setStatus] = useState('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(() => { diff --git a/src/components/PaymentPage/methods/GooglePay/Button.tsx b/src/components/PaymentPage/methods/GooglePay/Button.tsx index 921a0bd..7985c3b 100644 --- a/src/components/PaymentPage/methods/GooglePay/Button.tsx +++ b/src/components/PaymentPage/methods/GooglePay/Button.tsx @@ -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 ? : (
{ diff --git a/src/routes.ts b/src/routes.ts index c2e8ba6..fb70e6b 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -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()] diff --git a/src/store/index.ts b/src/store/index.ts index 139b3a9..9824419 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -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, } diff --git a/src/store/status.ts b/src/store/status.ts new file mode 100644 index 0000000..42d9805 --- /dev/null +++ b/src/store/status.ts @@ -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) { + 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 diff --git a/src/store/subscriptionPlan.ts b/src/store/subscriptionPlan.ts index 3ea8566..64e8bf4 100644 --- a/src/store/subscriptionPlan.ts +++ b/src/store/subscriptionPlan.ts @@ -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 diff --git a/src/store/token.ts b/src/store/token.ts index 8cd61b7..d9e619c 100644 --- a/src/store/token.ts +++ b/src/store/token.ts @@ -8,7 +8,7 @@ const authTokenSlice = createSlice({ name: 'token', initialState, reducers: { - update(state, action: PayloadAction) { + update(state, action: PayloadAction) { state = action.payload return state },