diff --git a/src/api/resources/UserPaymentIntents.ts b/src/api/resources/UserPaymentIntents.ts index 8fa9f7b..4b8df9b 100644 --- a/src/api/resources/UserPaymentIntents.ts +++ b/src/api/resources/UserPaymentIntents.ts @@ -1,6 +1,7 @@ -import routes from "../../routes" -import { AuthPayload } from "../types" -import { getAuthHeaders } from "../utils" +import { PaymentIntent } from '@chargebee/chargebee-js-types' +import { AuthPayload } from '../types' +import { getAuthHeaders } from '../utils' +import routes from '../../routes' export interface Payload extends AuthPayload { paymentMethod: PaymentMethod @@ -14,18 +15,7 @@ export interface Response { export type PaymentMethod = 'apple_pay' | 'google_pay' | 'card' export type CurrencyCode = 'USD' -export interface PaymentIntent { - id: string - status: 'inited' | 'in_progress' | 'authorized' | 'consumed' | 'expired' - amount: number - gateway_account_id: string - payment_method_type: PaymentMethod - expires_at: number - created_at: number - modified_at: number - currency_code: CurrencyCode - gateway: string -} + export interface Customer { id: string email: string diff --git a/src/api/resources/UserSubscriptionReceipts.ts b/src/api/resources/UserSubscriptionReceipts.ts index efb6636..d121f56 100644 --- a/src/api/resources/UserSubscriptionReceipts.ts +++ b/src/api/resources/UserSubscriptionReceipts.ts @@ -9,13 +9,13 @@ export interface GetPayload extends AuthPayload { export interface ChargebeeReceiptPayload extends AuthPayload { itemPriceId: string gwToken: string - referenceId: string + referenceId?: string } export interface AppleReceiptPayload extends AuthPayload { receiptData: string - autorenewable: boolean - sandbox: boolean + autorenewable?: boolean + sandbox?: boolean } export type Payload = ChargebeeReceiptPayload | AppleReceiptPayload @@ -48,7 +48,7 @@ export interface Response { } function createRequest({ token, itemPriceId, gwToken, referenceId }: ChargebeeReceiptPayload): Request -function createRequest({ token, receiptData, autorenewable, sandbox }: AppleReceiptPayload): Request +function createRequest({ token, receiptData, autorenewable = true, sandbox = true }: AppleReceiptPayload): Request function createRequest(payload: Payload): Request function createRequest(payload: Payload): Request { const url = new URL(routes.server.subscriptionReceipts()) diff --git a/src/api/types.ts b/src/api/types.ts index a1bd540..4a60c5f 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -12,7 +12,7 @@ export interface ApiError { } export interface ErrorResponse { - errors: AuthError[] | ApiError + errors: AuthError[] | ApiError | string[] } export type AuthToken = string diff --git a/src/api/useApiCall.ts b/src/api/useApiCall.ts index 2048539..afb8705 100644 --- a/src/api/useApiCall.ts +++ b/src/api/useApiCall.ts @@ -3,13 +3,13 @@ import { useState, useEffect } from "react"; interface HookResult { isPending: boolean error: Error | null - data: T + data: T | null } type ApiMethod = () => Promise export function useApiCall(apiMethod: ApiMethod): HookResult { - const [data, setData] = useState({} as T) + const [data, setData] = useState(null) const [error, setError] = useState(null) const [isPending, setIsPending] = useState(true) diff --git a/src/api/utils.ts b/src/api/utils.ts index f9cf60b..e6ad7e7 100644 --- a/src/api/utils.ts +++ b/src/api/utils.ts @@ -1,6 +1,13 @@ import { AuthToken } from "./types" import { ErrorResponse } from "./types" +class ApiError extends Error { + constructor(message: string) { + super(message) + this.name = this.constructor.name + } +} + export function createMethod(createRequest: (payload: P) => Request) { return async (payload: P): Promise => { const request = createRequest(payload) @@ -9,9 +16,10 @@ export function createMethod(createRequest: (payload: P) => Request) { if (response.ok) return data - const error = Array.isArray(data.errors) ? data.errors[0]?.title : data.errors.base[0] + const error = Array.isArray(data.errors) ? data.errors[0] : data.errors.base[0] + const errorMessage = typeof error === 'object' ? error.title : error - throw new Error(error) + throw new ApiError(errorMessage) } } diff --git a/src/components/PaymentPage/index.tsx b/src/components/PaymentPage/index.tsx index 2bc536f..51c518e 100644 --- a/src/components/PaymentPage/index.tsx +++ b/src/components/PaymentPage/index.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { selectors } from '../../store' @@ -12,10 +13,9 @@ import { } from './methods' import UserHeader from '../UserHeader' import Title from '../Title' +import Loader from '../Loader' import secure from './secure.png' import './styles.css' -import { useState } from 'react' -import Loader from '../Loader' function PaymentPage(): JSX.Element { const { t } = useTranslation() diff --git a/src/components/PaymentPage/methods/ApplePay/Apple-Pay.svg b/src/components/PaymentPage/methods/ApplePay/Apple-Pay.svg deleted file mode 100644 index 7baf5e2..0000000 --- a/src/components/PaymentPage/methods/ApplePay/Apple-Pay.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/src/components/PaymentPage/methods/ApplePay/Button.tsx b/src/components/PaymentPage/methods/ApplePay/Button.tsx index cadda22..835243a 100644 --- a/src/components/PaymentPage/methods/ApplePay/Button.tsx +++ b/src/components/PaymentPage/methods/ApplePay/Button.tsx @@ -1,14 +1,55 @@ +import { useCallback, useEffect, useId } from 'react' +import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' -import MainButton from '../../../MainButton' +import { PaymentIntent } from '@chargebee/chargebee-js-types' +import { useApi, useApiCall } from '../../../../api' +import { usePayment, ApplePayButtonOptions } from '../../../../payment' +import { useAuth } from '../../../../auth' +import Loader from '../../../Loader' +import ErrorText from '../../../ErrorText' import routes from '../../../../routes' -import ApplePay from './Apple-Pay.svg' + +const currencyCode = 'USD' +const paymentMethod = 'apple_pay' export function ApplePayButton(): JSX.Element { + const api = useApi() const navigate = useNavigate() - const handleClick = () => navigate(routes.client.wallpaper()) - return ( - - Apple Pay - + const buttonId = useId() + const { i18n } = useTranslation() + const { token } = useAuth() + const { applePay } = usePayment() + const loadData = useCallback(() => { + return api.createPaymentIntent({ token, paymentMethod, currencyCode }) + .then(({ payment_intent }) => payment_intent) + }, [api, token]) + const { data, error, isPending } = useApiCall(loadData) + + if (error) console.error(error) + + useEffect(() => { + if (data === null) return + const buttonOptions: ApplePayButtonOptions = { + buttonColor: 'black', + buttonType: 'pay', + locale: i18n.language + } + applePay?.setPaymentIntent(data) + 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]) + + return isPending ? : ( +
{ + error ? : null + }
) } diff --git a/src/components/PaymentPage/methods/Card/Modal.tsx b/src/components/PaymentPage/methods/Card/Modal.tsx index 39e430c..6d633c9 100644 --- a/src/components/PaymentPage/methods/Card/Modal.tsx +++ b/src/components/PaymentPage/methods/Card/Modal.tsx @@ -1,10 +1,13 @@ -import { useTranslation } from 'react-i18next' 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 { useAuth } from '../../../../auth' import Modal from '../../../Modal' import Title from '../../../Title' import MainButton from '../../../MainButton' @@ -15,6 +18,7 @@ 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 @@ -31,6 +35,11 @@ interface Field { type: string } +interface ChargebeeTokenizeResult { + token: string + vaultToken: string +} + type Status = 'idle' | 'loading' | 'filling' | 'tokenizing' | 'ready' | 'success' | 'error' const initCompletedFields = { @@ -43,10 +52,15 @@ type CompletedFields = typeof initCompletedFields const isReady = (fields: CompletedFields) => Object.values(fields).every((complete: boolean) => complete) +const itemPriceId = 'aura-membership-2-week-USD' + export function CardModal({ open, onClose }: CardModalProps): JSX.Element { + const api = useApi() + const navigate = useNavigate() const cardRef = useRef(null) const [status, setStatus] = useState('idle') const [fields, setFields] = useState(initCompletedFields) + const { token } = useAuth() const { t, i18n } = useTranslation() const locale = i18n.language const isInit = status === 'idle' @@ -65,7 +79,11 @@ export function CardModal({ open, onClose }: CardModalProps): JSX.Element { const payWithCard = () => { setStatus('tokenizing') cardRef.current?.tokenize({}) - .then(console.log) + .then((result: ChargebeeTokenizeResult) => { + return api.createSubscriptionReceipt({ token, itemPriceId, gwToken: result.token }) + }) + .then(() => console.log('Success payment by Card')) + .then(() => navigate(routes.client.wallpaper())) .catch(console.error) .finally(() => setStatus('success')) } diff --git a/src/components/PaymentPage/methods/GooglePay/Button.tsx b/src/components/PaymentPage/methods/GooglePay/Button.tsx index 2337f3e..e51c53e 100644 --- a/src/components/PaymentPage/methods/GooglePay/Button.tsx +++ b/src/components/PaymentPage/methods/GooglePay/Button.tsx @@ -1,14 +1,54 @@ +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 } from '../../../../api' +import { usePayment, GooglePayButtonOptions } from '../../../../payment' +import { useAuth } from '../../../../auth' +import Loader from '../../../Loader' +import ErrorText from '../../../ErrorText' import routes from '../../../../routes' -import MainButton from '../../../MainButton' -import GooglePay from './G-Pay.svg' + +const currencyCode = 'USD' +const paymentMethod = 'google_pay' export function GooglePayButton(): JSX.Element { + const api = useApi() const navigate = useNavigate() - const handleClick = () => navigate(routes.client.wallpaper()) - return ( - - Google Pay - + const buttonId = useId() + const { i18n } = useTranslation() + const { token } = useAuth() + const { googlePay } = usePayment() + const loadData = useCallback(() => { + return api.createPaymentIntent({ token, paymentMethod, currencyCode }) + .then(({ payment_intent }) => payment_intent) + }, [api, token]) + const { data, error, isPending } = useApiCall(loadData) + + if (error) console.error(error) + + useEffect(() => { + if (data === null) return + const buttonOptions: GooglePayButtonOptions = { + buttonColor: 'black', + buttonType: 'pay', + buttonLocale: i18n.language, + buttonSizeMode: 'fill', + } + googlePay?.setPaymentIntent(data) + googlePay?.mountPaymentButton(`#${buttonId}`, buttonOptions) + .then(() => googlePay?.handlePayment()) + .then((result) => { + 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]) + + return isPending ? : ( +
{ + error ? : null + }
) } diff --git a/src/components/PaymentPage/methods/GooglePay/G-Pay.svg b/src/components/PaymentPage/methods/GooglePay/G-Pay.svg deleted file mode 100644 index 2bd000b..0000000 --- a/src/components/PaymentPage/methods/GooglePay/G-Pay.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - diff --git a/src/components/StaticPage/index.tsx b/src/components/StaticPage/index.tsx index 7425101..6c27d44 100644 --- a/src/components/StaticPage/index.tsx +++ b/src/components/StaticPage/index.tsx @@ -17,11 +17,12 @@ function StaticPage(): JSX.Element { return api.getElement({ type, locale }) .then((resp: Element.Response) => resp.data.element) }, [api, typeId, locale]) - const { data, isPending, error } = useApiCall(loadData) + const { data, isPending, error } = useApiCall(loadData) + const content = data ? parse(data.body) : null return (
- {isPending ? :
{parse(data?.body)}
} + {isPending ? :
{content}
} {error && }
) diff --git a/src/components/SubscriptionPage/index.tsx b/src/components/SubscriptionPage/index.tsx index 30479a2..465d6f7 100644 --- a/src/components/SubscriptionPage/index.tsx +++ b/src/components/SubscriptionPage/index.tsx @@ -10,21 +10,22 @@ import UserHeader from '../UserHeader' import CallToAction from '../CallToAction' import routes from '../../routes' +const itemPriceId = 'aura-membership-2-week-USD' +const paymentItems = [ + { + title: 'Per 7-Day Trial For', + price: 1.00, + description: '2-Week Plan', + }, +] + function SubscriptionPage(): JSX.Element { const { t } = useTranslation() const navigate = useNavigate() const email = useSelector(selectors.selectEmail) - const itemPriceId = 'aura-membership-2-week-USD' const itemPrice = useSelector(selectors.selectPlanById(itemPriceId)) const currency = Currency.USD const locale = Locale.EN - const paymentItems = [ - { - title: 'Per 7-Day Trial For', - price: 1.00, - description: '2-Week Plan', - }, - ] const handleClick = () => navigate(routes.client.paymentMethod()) console.log({ itemPrice }) return ( diff --git a/src/components/WallpaperPage/index.tsx b/src/components/WallpaperPage/index.tsx index d794006..5e86506 100644 --- a/src/components/WallpaperPage/index.tsx +++ b/src/components/WallpaperPage/index.tsx @@ -1,6 +1,7 @@ import { useCallback } from 'react' -import { useApi, useApiCall, Assets, DailyForecasts } from '../../api' +import { useTranslation } from 'react-i18next' import { useAuth } from '../../auth' +import { useApi, useApiCall, Assets, DailyForecasts } from '../../api' import { saveFile, buildFilename } from './utils' import Loader, { LoaderColor } from '../Loader' import './styles.css' @@ -14,6 +15,7 @@ interface WallpaperData { function WallpaperPage(): JSX.Element { const api = useApi() + const { t } = useTranslation() const { user, token } = useAuth() const category = user?.profile.sign?.sign || '' const loadData = useCallback(() => { @@ -27,8 +29,8 @@ function WallpaperPage(): JSX.Element { })) }, [api, category, token]) const { data, isPending } = useApiCall(loadData) - const { assets, forecasts } = data - const asset = assets?.at(0) + const forecasts = data ? data.forecasts : [] + const asset = data ? data.assets.at(0) : null const handleClick = () => asset && saveFile(asset.url, buildFilename(category)) @@ -42,7 +44,7 @@ function WallpaperPage(): JSX.Element {
{isPending ? null : ( <> -

Analysis of personal background

+

{t('analysis_background')}

{forecasts.map((forecast) => (

{forecast.category}

diff --git a/src/locales/dev.ts b/src/locales/dev.ts index 04144c1..ac5a192 100644 --- a/src/locales/dev.ts +++ b/src/locales/dev.ts @@ -43,5 +43,6 @@ export default { will_be_charged: "You will be charged only . We'll email your a reminder before your trial period ends.", trial_price: "$1 for your 7-day trial", start_trial: "Start 7-Day Trial", + analysis_background: "Analysis of personal background", }, } diff --git a/src/payment/index.ts b/src/payment/index.ts index 9de7fa5..7258f41 100644 --- a/src/payment/index.ts +++ b/src/payment/index.ts @@ -1,2 +1,3 @@ export * from './PaymentContext' export * from './usePayment' +export * from './types' diff --git a/src/payment/types.ts b/src/payment/types.ts index 69888ac..36398ef 100644 --- a/src/payment/types.ts +++ b/src/payment/types.ts @@ -1,18 +1,42 @@ import { PaymentIntent } from '@chargebee/chargebee-js-types' interface Handler { - mountPaymentButton: (id: string) => Promise handlePayment: () => Promise + setPaymentIntent: (paymentIntent: PaymentIntent) => void +} + +export interface GooglePayButtonOptions { + buttonColor: 'default' | 'black' | 'white' + buttonType: 'long' | 'short' | 'book' | 'buy' | 'checkout' | 'donate' | 'order' | 'pay' | 'plain' | 'subscribe' + buttonSizeMode: 'static' | 'fill' + buttonLocale: string +} + +export interface PaymentOptions { + requestPayerEmail: boolean + requestBillingAddress: boolean + requestShippingAddress: boolean +} + +export interface ApplePayButtonOptions { + locale: string + buttonColor: 'black' | 'white' | 'white-outline' + buttonType: 'add-money' | 'book' | 'buy' | 'check-out' | 'continue' | 'contribute' | 'donate' | 'order' | 'pay' | 'plain' | 'reload' | 'rent' | 'set-up' | 'subscribe' | 'support' | 'tip' | 'top-up' } interface ApplePay extends Handler { canMakePayments: () => boolean + mountPaymentButton: (selector: string, options?: ApplePayButtonOptions) => Promise } interface GooglePay extends Handler { getPaymentIntent: () => PaymentIntent - setPaymentIntent: (paymentIntent: PaymentIntent) => void updatePaymentIntent: (paymentIntent: PaymentIntent) => void + mountPaymentButton: ( + selector: string, + buttonOptions?: GooglePayButtonOptions, + paymentOptions?: PaymentOptions + ) => Promise } export type ApplePayHandler = ApplePay | null