feat: apple pay, google pay buttons and card tokenize
This commit is contained in:
parent
0cf8f2898d
commit
888b081393
@ -1,6 +1,7 @@
|
|||||||
import routes from "../../routes"
|
import { PaymentIntent } from '@chargebee/chargebee-js-types'
|
||||||
import { AuthPayload } from "../types"
|
import { AuthPayload } from '../types'
|
||||||
import { getAuthHeaders } from "../utils"
|
import { getAuthHeaders } from '../utils'
|
||||||
|
import routes from '../../routes'
|
||||||
|
|
||||||
export interface Payload extends AuthPayload {
|
export interface Payload extends AuthPayload {
|
||||||
paymentMethod: PaymentMethod
|
paymentMethod: PaymentMethod
|
||||||
@ -14,18 +15,7 @@ export interface Response {
|
|||||||
|
|
||||||
export type PaymentMethod = 'apple_pay' | 'google_pay' | 'card'
|
export type PaymentMethod = 'apple_pay' | 'google_pay' | 'card'
|
||||||
export type CurrencyCode = 'USD'
|
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 {
|
export interface Customer {
|
||||||
id: string
|
id: string
|
||||||
email: string
|
email: string
|
||||||
|
|||||||
@ -9,13 +9,13 @@ export interface GetPayload extends AuthPayload {
|
|||||||
export interface ChargebeeReceiptPayload extends AuthPayload {
|
export interface ChargebeeReceiptPayload extends AuthPayload {
|
||||||
itemPriceId: string
|
itemPriceId: string
|
||||||
gwToken: string
|
gwToken: string
|
||||||
referenceId: string
|
referenceId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppleReceiptPayload extends AuthPayload {
|
export interface AppleReceiptPayload extends AuthPayload {
|
||||||
receiptData: string
|
receiptData: string
|
||||||
autorenewable: boolean
|
autorenewable?: boolean
|
||||||
sandbox: boolean
|
sandbox?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Payload = ChargebeeReceiptPayload | AppleReceiptPayload
|
export type Payload = ChargebeeReceiptPayload | AppleReceiptPayload
|
||||||
@ -48,7 +48,7 @@ export interface Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createRequest({ token, itemPriceId, gwToken, referenceId }: ChargebeeReceiptPayload): Request
|
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
|
||||||
function createRequest(payload: Payload): Request {
|
function createRequest(payload: Payload): Request {
|
||||||
const url = new URL(routes.server.subscriptionReceipts())
|
const url = new URL(routes.server.subscriptionReceipts())
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export interface ApiError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorResponse {
|
export interface ErrorResponse {
|
||||||
errors: AuthError[] | ApiError
|
errors: AuthError[] | ApiError | string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthToken = string
|
export type AuthToken = string
|
||||||
|
|||||||
@ -3,13 +3,13 @@ import { useState, useEffect } from "react";
|
|||||||
interface HookResult<T> {
|
interface HookResult<T> {
|
||||||
isPending: boolean
|
isPending: boolean
|
||||||
error: Error | null
|
error: Error | null
|
||||||
data: T
|
data: T | null
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApiMethod<T> = () => Promise<T>
|
type ApiMethod<T> = () => Promise<T>
|
||||||
|
|
||||||
export function useApiCall<T>(apiMethod: ApiMethod<T>): HookResult<T> {
|
export function useApiCall<T>(apiMethod: ApiMethod<T>): HookResult<T> {
|
||||||
const [data, setData] = useState<T>({} as T)
|
const [data, setData] = useState<T | null>(null)
|
||||||
const [error, setError] = useState<Error | null>(null)
|
const [error, setError] = useState<Error | null>(null)
|
||||||
const [isPending, setIsPending] = useState<boolean>(true)
|
const [isPending, setIsPending] = useState<boolean>(true)
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
import { AuthToken } from "./types"
|
import { AuthToken } from "./types"
|
||||||
import { ErrorResponse } from "./types"
|
import { ErrorResponse } from "./types"
|
||||||
|
|
||||||
|
class ApiError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message)
|
||||||
|
this.name = this.constructor.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createMethod<P, R>(createRequest: (payload: P) => Request) {
|
export function createMethod<P, R>(createRequest: (payload: P) => Request) {
|
||||||
return async (payload: P): Promise<R> => {
|
return async (payload: P): Promise<R> => {
|
||||||
const request = createRequest(payload)
|
const request = createRequest(payload)
|
||||||
@ -9,9 +16,10 @@ export function createMethod<P, R>(createRequest: (payload: P) => Request) {
|
|||||||
|
|
||||||
if (response.ok) return data
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { selectors } from '../../store'
|
import { selectors } from '../../store'
|
||||||
@ -12,10 +13,9 @@ import {
|
|||||||
} from './methods'
|
} from './methods'
|
||||||
import UserHeader from '../UserHeader'
|
import UserHeader from '../UserHeader'
|
||||||
import Title from '../Title'
|
import Title from '../Title'
|
||||||
|
import Loader from '../Loader'
|
||||||
import secure from './secure.png'
|
import secure from './secure.png'
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
import { useState } from 'react'
|
|
||||||
import Loader from '../Loader'
|
|
||||||
|
|
||||||
function PaymentPage(): JSX.Element {
|
function PaymentPage(): JSX.Element {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<svg version="1.1" baseProfile="tiny" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
x="0px" y="0px" viewBox="0 0 512 210.2" xml:space="preserve">
|
|
||||||
<path fill="#fff" id="XMLID_34_" d="M93.6,27.1C87.6,34.2,78,39.8,68.4,39c-1.2-9.6,3.5-19.8,9-26.1c6-7.3,16.5-12.5,25-12.9
|
|
||||||
C103.4,10,99.5,19.8,93.6,27.1 M102.3,40.9c-13.9-0.8-25.8,7.9-32.4,7.9c-6.7,0-16.8-7.5-27.8-7.3c-14.3,0.2-27.6,8.3-34.9,21.2
|
|
||||||
c-15,25.8-3.9,64,10.6,85c7.1,10.4,15.6,21.8,26.8,21.4c10.6-0.4,14.8-6.9,27.6-6.9c12.9,0,16.6,6.9,27.8,6.7
|
|
||||||
c11.6-0.2,18.9-10.4,26-20.8c8.1-11.8,11.4-23.3,11.6-23.9c-0.2-0.2-22.4-8.7-22.6-34.3c-0.2-21.4,17.5-31.6,18.3-32.2
|
|
||||||
C123.3,42.9,107.7,41.3,102.3,40.9 M182.6,11.9v155.9h24.2v-53.3h33.5c30.6,0,52.1-21,52.1-51.4c0-30.4-21.1-51.2-51.3-51.2H182.6z
|
|
||||||
M206.8,32.3h27.9c21,0,33,11.2,33,30.9c0,19.7-12,31-33.1,31h-27.8V32.3z M336.6,169c15.2,0,29.3-7.7,35.7-19.9h0.5v18.7h22.4V90.2
|
|
||||||
c0-22.5-18-37-45.7-37c-25.7,0-44.7,14.7-45.4,34.9h21.8c1.8-9.6,10.7-15.9,22.9-15.9c14.8,0,23.1,6.9,23.1,19.6v8.6l-30.2,1.8
|
|
||||||
c-28.1,1.7-43.3,13.2-43.3,33.2C298.4,155.6,314.1,169,336.6,169z M343.1,150.5c-12.9,0-21.1-6.2-21.1-15.7c0-9.8,7.9-15.5,23-16.4
|
|
||||||
l26.9-1.7v8.8C371.9,140.1,359.5,150.5,343.1,150.5z M425.1,210.2c23.6,0,34.7-9,44.4-36.3L512,54.7h-24.6l-28.5,92.1h-0.5
|
|
||||||
l-28.5-92.1h-25.3l41,113.5l-2.2,6.9c-3.7,11.7-9.7,16.2-20.4,16.2c-1.9,0-5.6-0.2-7.1-0.4v18.7C417.3,210,423.3,210.2,425.1,210.2z
|
|
||||||
"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@ -1,14 +1,55 @@
|
|||||||
|
import { useCallback, useEffect, useId } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router-dom'
|
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 routes from '../../../../routes'
|
||||||
import ApplePay from './Apple-Pay.svg'
|
|
||||||
|
const currencyCode = 'USD'
|
||||||
|
const paymentMethod = 'apple_pay'
|
||||||
|
|
||||||
export function ApplePayButton(): JSX.Element {
|
export function ApplePayButton(): JSX.Element {
|
||||||
|
const api = useApi()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const handleClick = () => navigate(routes.client.wallpaper())
|
const buttonId = useId()
|
||||||
return (
|
const { i18n } = useTranslation()
|
||||||
<MainButton onClick={handleClick}>
|
const { token } = useAuth()
|
||||||
<img className='payment-btn' src={ApplePay} alt='Apple Pay' />
|
const { applePay } = usePayment()
|
||||||
</MainButton>
|
const loadData = useCallback(() => {
|
||||||
|
return api.createPaymentIntent({ token, paymentMethod, currencyCode })
|
||||||
|
.then(({ payment_intent }) => payment_intent)
|
||||||
|
}, [api, token])
|
||||||
|
const { data, error, isPending } = useApiCall<PaymentIntent>(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 ? <Loader /> : (
|
||||||
|
<div id={buttonId} style={{ height: 60 }}>{
|
||||||
|
error ? <ErrorText message={error.message} isShown={true} size='large'/> : null
|
||||||
|
}</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useEffect, useRef, useState, ChangeEvent } from 'react'
|
import { useEffect, useRef, useState, ChangeEvent } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
CardCVV, CardComponent, CardExpiry, CardNumber, Provider
|
CardCVV, CardComponent, CardExpiry, CardNumber, Provider
|
||||||
} from '@chargebee/chargebee-js-react-wrapper'
|
} from '@chargebee/chargebee-js-react-wrapper'
|
||||||
import ChargebeeComponents from '@chargebee/chargebee-js-react-wrapper/dist/components/ComponentGroup'
|
import ChargebeeComponents from '@chargebee/chargebee-js-react-wrapper/dist/components/ComponentGroup'
|
||||||
import { usePayment } from '../../../../payment'
|
import { usePayment } from '../../../../payment'
|
||||||
|
import { useApi } from '../../../../api'
|
||||||
|
import { useAuth } from '../../../../auth'
|
||||||
import Modal from '../../../Modal'
|
import Modal from '../../../Modal'
|
||||||
import Title from '../../../Title'
|
import Title from '../../../Title'
|
||||||
import MainButton from '../../../MainButton'
|
import MainButton from '../../../MainButton'
|
||||||
@ -15,6 +18,7 @@ import amex from './amex.svg'
|
|||||||
import diners from './diners.svg'
|
import diners from './diners.svg'
|
||||||
import discover from './discover.svg'
|
import discover from './discover.svg'
|
||||||
import { cardStyles } from './styles'
|
import { cardStyles } from './styles'
|
||||||
|
import routes from '../../../../routes'
|
||||||
|
|
||||||
interface CardModalProps {
|
interface CardModalProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@ -31,6 +35,11 @@ interface Field {
|
|||||||
type: string
|
type: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ChargebeeTokenizeResult {
|
||||||
|
token: string
|
||||||
|
vaultToken: string
|
||||||
|
}
|
||||||
|
|
||||||
type Status = 'idle' | 'loading' | 'filling' | 'tokenizing' | 'ready' | 'success' | 'error'
|
type Status = 'idle' | 'loading' | 'filling' | 'tokenizing' | 'ready' | 'success' | 'error'
|
||||||
|
|
||||||
const initCompletedFields = {
|
const initCompletedFields = {
|
||||||
@ -43,10 +52,15 @@ type CompletedFields = typeof initCompletedFields
|
|||||||
|
|
||||||
const isReady = (fields: CompletedFields) => Object.values(fields).every((complete: boolean) => complete)
|
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 {
|
export function CardModal({ open, onClose }: CardModalProps): JSX.Element {
|
||||||
|
const api = useApi()
|
||||||
|
const navigate = useNavigate()
|
||||||
const cardRef = useRef<ChargebeeComponents>(null)
|
const cardRef = useRef<ChargebeeComponents>(null)
|
||||||
const [status, setStatus] = useState<Status>('idle')
|
const [status, setStatus] = useState<Status>('idle')
|
||||||
const [fields, setFields] = useState(initCompletedFields)
|
const [fields, setFields] = useState(initCompletedFields)
|
||||||
|
const { token } = useAuth()
|
||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
const locale = i18n.language
|
const locale = i18n.language
|
||||||
const isInit = status === 'idle'
|
const isInit = status === 'idle'
|
||||||
@ -65,7 +79,11 @@ export function CardModal({ open, onClose }: CardModalProps): JSX.Element {
|
|||||||
const payWithCard = () => {
|
const payWithCard = () => {
|
||||||
setStatus('tokenizing')
|
setStatus('tokenizing')
|
||||||
cardRef.current?.tokenize({})
|
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)
|
.catch(console.error)
|
||||||
.finally(() => setStatus('success'))
|
.finally(() => setStatus('success'))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,54 @@
|
|||||||
|
import { useCallback, useEffect, useId } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router-dom'
|
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 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 {
|
export function GooglePayButton(): JSX.Element {
|
||||||
|
const api = useApi()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const handleClick = () => navigate(routes.client.wallpaper())
|
const buttonId = useId()
|
||||||
return (
|
const { i18n } = useTranslation()
|
||||||
<MainButton onClick={handleClick}>
|
const { token } = useAuth()
|
||||||
<img className='payment-btn' src={GooglePay} alt='Google Pay' />
|
const { googlePay } = usePayment()
|
||||||
</MainButton>
|
const loadData = useCallback(() => {
|
||||||
|
return api.createPaymentIntent({ token, paymentMethod, currencyCode })
|
||||||
|
.then(({ payment_intent }) => payment_intent)
|
||||||
|
}, [api, token])
|
||||||
|
const { data, error, isPending } = useApiCall<PaymentIntent>(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 ? <Loader /> : (
|
||||||
|
<div id={buttonId} style={{ height: 60 }}>{
|
||||||
|
error ? <ErrorText message={error.message} isShown={true} size='large'/> : null
|
||||||
|
}</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 80 38.1" style="enable-background:new 0 0 80 38.1;" xml:space="preserve">
|
|
||||||
<style type="text/css">
|
|
||||||
.st0{fill:#FFF;}
|
|
||||||
.st1{fill:#4285F4;}
|
|
||||||
.st2{fill:#34A853;}
|
|
||||||
.st3{fill:#FBBC04;}
|
|
||||||
.st4{fill:#EA4335;}
|
|
||||||
</style>
|
|
||||||
<path class="st0" d="M37.8,19.7V29h-3V6h7.8c1.9,0,3.7,0.7,5.1,2c1.4,1.2,2.1,3,2.1,4.9c0,1.9-0.7,3.6-2.1,4.9c-1.4,1.3-3.1,2-5.1,2
|
|
||||||
L37.8,19.7L37.8,19.7z M37.8,8.8v8h5c1.1,0,2.2-0.4,2.9-1.2c1.6-1.5,1.6-4,0.1-5.5c0,0-0.1-0.1-0.1-0.1c-0.8-0.8-1.8-1.3-2.9-1.2
|
|
||||||
L37.8,8.8L37.8,8.8z"/>
|
|
||||||
<path class="st0" d="M56.7,12.8c2.2,0,3.9,0.6,5.2,1.8s1.9,2.8,1.9,4.8V29H61v-2.2h-0.1c-1.2,1.8-2.9,2.7-4.9,2.7
|
|
||||||
c-1.7,0-3.2-0.5-4.4-1.5c-1.1-1-1.8-2.4-1.8-3.9c0-1.6,0.6-2.9,1.8-3.9c1.2-1,2.9-1.4,4.9-1.4c1.8,0,3.2,0.3,4.3,1v-0.7
|
|
||||||
c0-1-0.4-2-1.2-2.6c-0.8-0.7-1.8-1.1-2.9-1.1c-1.7,0-3,0.7-3.9,2.1l-2.6-1.6C51.8,13.8,53.9,12.8,56.7,12.8z M52.9,24.2
|
|
||||||
c0,0.8,0.4,1.5,1,1.9c0.7,0.5,1.5,0.8,2.3,0.8c1.2,0,2.4-0.5,3.3-1.4c1-0.9,1.5-2,1.5-3.2c-0.9-0.7-2.2-1.1-3.9-1.1
|
|
||||||
c-1.2,0-2.2,0.3-3,0.9C53.3,22.6,52.9,23.3,52.9,24.2z"/>
|
|
||||||
<path class="st0" d="M80,13.3l-9.9,22.7h-3l3.7-7.9l-6.5-14.7h3.2l4.7,11.3h0.1l4.6-11.3H80z"/>
|
|
||||||
<path class="st1" d="M25.9,17.7c0-0.9-0.1-1.8-0.2-2.7H13.2v5.1h7.1c-0.3,1.6-1.2,3.1-2.6,4v3.3H22C24.5,25.1,25.9,21.7,25.9,17.7z"
|
|
||||||
/>
|
|
||||||
<path class="st2" d="M13.2,30.6c3.6,0,6.6-1.2,8.8-3.2l-4.3-3.3c-1.2,0.8-2.7,1.3-4.5,1.3c-3.4,0-6.4-2.3-7.4-5.5H1.4v3.4
|
|
||||||
C3.7,27.8,8.2,30.6,13.2,30.6z"/>
|
|
||||||
<path class="st3" d="M5.8,19.9c-0.6-1.6-0.6-3.4,0-5.1v-3.4H1.4c-1.9,3.7-1.9,8.1,0,11.9L5.8,19.9z"/>
|
|
||||||
<path class="st4" d="M13.2,9.4c1.9,0,3.7,0.7,5.1,2l0,0l3.8-3.8c-2.4-2.2-5.6-3.5-8.8-3.4c-5,0-9.6,2.8-11.8,7.3l4.4,3.4
|
|
||||||
C6.8,11.7,9.8,9.4,13.2,9.4z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@ -17,11 +17,12 @@ function StaticPage(): JSX.Element {
|
|||||||
return api.getElement({ type, locale })
|
return api.getElement({ type, locale })
|
||||||
.then((resp: Element.Response) => resp.data.element)
|
.then((resp: Element.Response) => resp.data.element)
|
||||||
}, [api, typeId, locale])
|
}, [api, typeId, locale])
|
||||||
const { data, isPending, error } = useApiCall(loadData)
|
const { data, isPending, error } = useApiCall<Element.Element>(loadData)
|
||||||
|
const content = data ? parse(data.body) : null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className='page page-static'>
|
<section className='page page-static'>
|
||||||
{isPending ? <Loader /> : <div className='page-static__content'>{parse(data?.body)}</div>}
|
{isPending ? <Loader /> : <div className='page-static__content'>{content}</div>}
|
||||||
{error && <NotFoundPage />}
|
{error && <NotFoundPage />}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -10,21 +10,22 @@ import UserHeader from '../UserHeader'
|
|||||||
import CallToAction from '../CallToAction'
|
import CallToAction from '../CallToAction'
|
||||||
import routes from '../../routes'
|
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 {
|
function SubscriptionPage(): JSX.Element {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const email = useSelector(selectors.selectEmail)
|
const email = useSelector(selectors.selectEmail)
|
||||||
const itemPriceId = 'aura-membership-2-week-USD'
|
|
||||||
const itemPrice = useSelector(selectors.selectPlanById(itemPriceId))
|
const itemPrice = useSelector(selectors.selectPlanById(itemPriceId))
|
||||||
const currency = Currency.USD
|
const currency = Currency.USD
|
||||||
const locale = Locale.EN
|
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())
|
const handleClick = () => navigate(routes.client.paymentMethod())
|
||||||
console.log({ itemPrice })
|
console.log({ itemPrice })
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useApi, useApiCall, Assets, DailyForecasts } from '../../api'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAuth } from '../../auth'
|
import { useAuth } from '../../auth'
|
||||||
|
import { useApi, useApiCall, Assets, DailyForecasts } from '../../api'
|
||||||
import { saveFile, buildFilename } from './utils'
|
import { saveFile, buildFilename } from './utils'
|
||||||
import Loader, { LoaderColor } from '../Loader'
|
import Loader, { LoaderColor } from '../Loader'
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
@ -14,6 +15,7 @@ interface WallpaperData {
|
|||||||
|
|
||||||
function WallpaperPage(): JSX.Element {
|
function WallpaperPage(): JSX.Element {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
const { t } = useTranslation()
|
||||||
const { user, token } = useAuth()
|
const { user, token } = useAuth()
|
||||||
const category = user?.profile.sign?.sign || ''
|
const category = user?.profile.sign?.sign || ''
|
||||||
const loadData = useCallback(() => {
|
const loadData = useCallback(() => {
|
||||||
@ -27,8 +29,8 @@ function WallpaperPage(): JSX.Element {
|
|||||||
}))
|
}))
|
||||||
}, [api, category, token])
|
}, [api, category, token])
|
||||||
const { data, isPending } = useApiCall<WallpaperData>(loadData)
|
const { data, isPending } = useApiCall<WallpaperData>(loadData)
|
||||||
const { assets, forecasts } = data
|
const forecasts = data ? data.forecasts : []
|
||||||
const asset = assets?.at(0)
|
const asset = data ? data.assets.at(0) : null
|
||||||
|
|
||||||
const handleClick = () => asset && saveFile(asset.url, buildFilename(category))
|
const handleClick = () => asset && saveFile(asset.url, buildFilename(category))
|
||||||
|
|
||||||
@ -42,7 +44,7 @@ function WallpaperPage(): JSX.Element {
|
|||||||
<div className='wallpaper-content'>
|
<div className='wallpaper-content'>
|
||||||
{isPending ? null : (
|
{isPending ? null : (
|
||||||
<>
|
<>
|
||||||
<h1 className='wallpaper-title'>Analysis of personal background</h1>
|
<h1 className='wallpaper-title'>{t('analysis_background')}</h1>
|
||||||
{forecasts.map((forecast) => (
|
{forecasts.map((forecast) => (
|
||||||
<div key={forecast.category_name} className='wallpaper-forecast'>
|
<div key={forecast.category_name} className='wallpaper-forecast'>
|
||||||
<h2 className='wallpaper-subtitle'>{forecast.category}</h2>
|
<h2 className='wallpaper-subtitle'>{forecast.category}</h2>
|
||||||
|
|||||||
@ -43,5 +43,6 @@ export default {
|
|||||||
will_be_charged: "You will be charged only <strongText>. We'll email your a reminder before your trial period ends.",
|
will_be_charged: "You will be charged only <strongText>. We'll email your a reminder before your trial period ends.",
|
||||||
trial_price: "$1 for your 7-day trial",
|
trial_price: "$1 for your 7-day trial",
|
||||||
start_trial: "Start 7-Day Trial",
|
start_trial: "Start 7-Day Trial",
|
||||||
|
analysis_background: "Analysis of personal background",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from './PaymentContext'
|
export * from './PaymentContext'
|
||||||
export * from './usePayment'
|
export * from './usePayment'
|
||||||
|
export * from './types'
|
||||||
|
|||||||
@ -1,18 +1,42 @@
|
|||||||
import { PaymentIntent } from '@chargebee/chargebee-js-types'
|
import { PaymentIntent } from '@chargebee/chargebee-js-types'
|
||||||
|
|
||||||
interface Handler {
|
interface Handler {
|
||||||
mountPaymentButton: (id: string) => Promise<void>
|
|
||||||
handlePayment: () => Promise<PaymentIntent>
|
handlePayment: () => Promise<PaymentIntent>
|
||||||
|
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 {
|
interface ApplePay extends Handler {
|
||||||
canMakePayments: () => boolean
|
canMakePayments: () => boolean
|
||||||
|
mountPaymentButton: (selector: string, options?: ApplePayButtonOptions) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GooglePay extends Handler {
|
interface GooglePay extends Handler {
|
||||||
getPaymentIntent: () => PaymentIntent
|
getPaymentIntent: () => PaymentIntent
|
||||||
setPaymentIntent: (paymentIntent: PaymentIntent) => void
|
|
||||||
updatePaymentIntent: (paymentIntent: PaymentIntent) => void
|
updatePaymentIntent: (paymentIntent: PaymentIntent) => void
|
||||||
|
mountPaymentButton: (
|
||||||
|
selector: string,
|
||||||
|
buttonOptions?: GooglePayButtonOptions,
|
||||||
|
paymentOptions?: PaymentOptions
|
||||||
|
) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApplePayHandler = ApplePay | null
|
export type ApplePayHandler = ApplePay | null
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user