diff --git a/public/ExclamationIcon.png b/public/ExclamationIcon.png new file mode 100644 index 0000000..a098232 Binary files /dev/null and b/public/ExclamationIcon.png differ diff --git a/public/SuccessIcon.png b/public/SuccessIcon.png new file mode 100644 index 0000000..2110b7e Binary files /dev/null and b/public/SuccessIcon.png differ diff --git a/src/api/resources/UserSubscriptionReceipts.ts b/src/api/resources/UserSubscriptionReceipts.ts index 6ed5948..3fc8138 100644 --- a/src/api/resources/UserSubscriptionReceipts.ts +++ b/src/api/resources/UserSubscriptionReceipts.ts @@ -1,85 +1,121 @@ -import routes from "@/routes" -import { AuthPayload } from "../types" -import { getAuthHeaders } from "../utils" +import routes from "@/routes"; +import { AuthPayload } from "../types"; +import { getAuthHeaders } from "../utils"; export interface GetPayload extends AuthPayload { - id: string + id: string; } export interface ChargebeeReceiptPayload extends AuthPayload { - itemPriceId: string - gwToken: string - referenceId?: string + itemPriceId: string; + gwToken: string; + referenceId?: string; } export interface AppleReceiptPayload extends AuthPayload { - receiptData: string - autorenewable?: boolean - sandbox?: boolean + receiptData: string; + autorenewable?: boolean; + sandbox?: boolean; } -export type Payload = ChargebeeReceiptPayload | AppleReceiptPayload +export interface StripeReceiptPayload extends AuthPayload { + itemInterval: "week" | "month" | "year"; +} + +export type Payload = + | ChargebeeReceiptPayload + | AppleReceiptPayload + | StripeReceiptPayload; export interface Response { - subscription_receipt: SubscriptionReceipt + subscription_receipt: SubscriptionReceipt; } export interface SubscriptionReceipt { - id: string - user_id: number - status: number - expires_at: null | string - requested_at: string - created_at: string + 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 + item_price_id: string; } - ], + ]; payment_intent: { - gw_token: string - gateway_account_id: string - } - }, - app_bundle_id: string - autorenewable: boolean - error: string - } + gw_token: string; + gateway_account_id: string; + }; + }; + client_secret: string; + app_bundle_id: string; + autorenewable: boolean; + error: string; + }; } -function createRequest({ token, itemPriceId, gwToken, referenceId }: ChargebeeReceiptPayload): Request -function createRequest({ token, receiptData, autorenewable = true, sandbox = true }: AppleReceiptPayload): Request -function createRequest(payload: Payload): Request +function createRequest({ + token, + itemPriceId, + gwToken, + referenceId, +}: ChargebeeReceiptPayload): Request; +function createRequest({ + token, + receiptData, + autorenewable = true, + sandbox = true, +}: AppleReceiptPayload): Request; +function createRequest({ token, itemInterval }: StripeReceiptPayload): Request; +function createRequest(payload: Payload): Request; function createRequest(payload: Payload): Request { - const url = new URL(routes.server.subscriptionReceipts()) - const data = isChargebeeReceipt(payload) ? { - way: 'chargebee', - subscription_receipt: { - item_price_id: payload.itemPriceId, - gw_token: payload.gwToken, - reference_id: payload.referenceId, - } - } : { - way: 'apple', - subscription_receipt: { - receipt_data: payload.receiptData, - autorenewable: payload.autorenewable, - sandbox: payload.sandbox, - } - } - const body = JSON.stringify(data) - return new Request(url, { method: 'POST', headers: getAuthHeaders(payload.token), body }) + const url = new URL(routes.server.subscriptionReceipts()); + const data = getDataPayload(payload); + const body = JSON.stringify(data); + return new Request(url, { + method: "POST", + headers: getAuthHeaders(payload.token), + body, + }); } -function isChargebeeReceipt(payload: Payload ): payload is ChargebeeReceiptPayload { - return 'itemPriceId' in payload && 'gwToken' in payload +function getDataPayload(payload: Payload) { + if ("itemPriceId" in payload && "gwToken" in payload) { + return { + way: "chargebee", + subscription_receipt: { + item_price_id: payload.itemPriceId, + gw_token: payload.gwToken, + reference_id: payload.referenceId, + }, + }; + } + if ("receiptData" in payload) { + return { + way: "apple", + subscription_receipt: { + receipt_data: payload.receiptData, + autorenewable: payload.autorenewable, + sandbox: payload.sandbox, + }, + }; + } + if ("itemInterval" in payload) { + return { + way: "stripe", + subscription_receipt: { + item_interval: payload.itemInterval, + }, + }; + } } function createGetRequest({ id, token }: GetPayload): Request { - const url = new URL(routes.server.subscriptionReceipt(id)) - return new Request(url, { method: 'GET', headers: getAuthHeaders(token) }) + const url = new URL(routes.server.subscriptionReceipt(id)); + return new Request(url, { method: "GET", headers: getAuthHeaders(token) }); } -export { createRequest, createGetRequest } +export { createRequest, createGetRequest }; diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 2391167..5698856 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -45,12 +45,16 @@ import { EPathsFromHome } from "@/store/siteConfig"; import parseAPNG, { APNG } from "apng-js"; import { useApi, useApiCall } from "@/api"; import { Asset } from "@/api/resources/Assets"; +import PaymentResultPage from "../PaymentPage/results"; +import PaymentSuccessPage from "../PaymentPage/results/SuccessPage"; +import PaymentFailPage from "../PaymentPage/results/ErrorPage"; function App(): JSX.Element { const [isSpecialOfferOpen, setIsSpecialOfferOpen] = useState(false); const [leoApng, setLeoApng] = useState(Error); const navigate = useNavigate(); const api = useApi(); + const dispatch = useDispatch(); const closeSpecialOfferAttention = () => { setIsSpecialOfferOpen(false); @@ -66,6 +70,15 @@ function App(): JSX.Element { const { data } = useApiCall(assetsData); + useEffect(() => { + // TODO: remove later + dispatch( + actions.token.update( + "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjQwNjEyLCJpYXQiOjE2OTc5MjY0MTksImV4cCI6MTcwNjU2NjQxOSwianRpIjoiZTg0NWE0ZmUtYmVmNy00ODNmLWIwMzgtYjlkYzBlZjk1MjNmIiwiZW1haWwiOiJvdGhlcjJAZXhhbXBsZS5jb20iLCJzdGF0ZSI6InByb3ZlbiIsImxvYyI6ImVuIiwidHoiOjAsInR5cGUiOiJlbWFpbCIsImlzcyI6ImNvbS5saWZlLmF1cmEifQ.ijaHDiNRLUIKdkziVB-zt8DA8WNH7RNwvYkp2EGDxTM" + ) + ); + }, [dispatch]); + useEffect(() => { async function getApng() { if (!data) return; @@ -125,6 +138,9 @@ function App(): JSX.Element { element={} /> } /> + } /> + } /> + } /> } diff --git a/src/components/BreathPage/index.tsx b/src/components/BreathPage/index.tsx index e530a8a..ca23057 100644 --- a/src/components/BreathPage/index.tsx +++ b/src/components/BreathPage/index.tsx @@ -67,8 +67,8 @@ function BreathPage({ leoApng }: BreathPageProps): JSX.Element { setIsOpenModal(false); }; - const token = - "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjIzNjEyLCJpYXQiOjE2OTM0MTg5MTAsImV4cCI6MTcwMjA1ODkxMCwianRpIjoiNzg5MjkwYWItODg0YS00MGUyLTkyNjEtOWI2OGEyNjkwNmE0IiwiZW1haWwiOiJvdGhlckBleGFtcGxlLmNvbSIsInN0YXRlIjoicHJvdmVuIiwibG9jIjoiZW4iLCJ0eiI6LTI4ODAwLCJ0eXBlIjoiZW1haWwiLCJpc3MiOiJjb20ubGlmZS5hdXJhIn0.J2ocWIv5jKzuKMcwMgWMiNMyGg5qLlMAeln-bQm_9lw"; + + const token = useSelector(selectors.selectToken) const createCallback = useCallback(async () => { const data: UserCallbacks.PayloadPost = { data: { @@ -100,7 +100,7 @@ function BreathPage({ leoApng }: BreathPageProps): JSX.Element { } return createCallbackRequest.user_callback; - }, [api, dispatch]); + }, [api, dispatch, token]); useApiCall(createCallback); diff --git a/src/components/CompatResultPage/index.tsx b/src/components/CompatResultPage/index.tsx index 92a1249..16a737a 100644 --- a/src/components/CompatResultPage/index.tsx +++ b/src/components/CompatResultPage/index.tsx @@ -12,8 +12,7 @@ import FullScreenModal from "../FullScreenModal"; import CompatibilityLoading from "../CompatibilityLoading"; function CompatResultPage(): JSX.Element { - const token = - "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjIzNjEyLCJpYXQiOjE2OTM0MTg5MTAsImV4cCI6MTcwMjA1ODkxMCwianRpIjoiNzg5MjkwYWItODg0YS00MGUyLTkyNjEtOWI2OGEyNjkwNmE0IiwiZW1haWwiOiJvdGhlckBleGFtcGxlLmNvbSIsInN0YXRlIjoicHJvdmVuIiwibG9jIjoiZW4iLCJ0eiI6LTI4ODAwLCJ0eXBlIjoiZW1haWwiLCJpc3MiOiJjb20ubGlmZS5hdXJhIn0.J2ocWIv5jKzuKMcwMgWMiNMyGg5qLlMAeln-bQm_9lw"; + const token = useSelector(selectors.selectToken) const { t } = useTranslation(); const navigate = useNavigate(); const api = useApi(); @@ -82,7 +81,7 @@ function CompatResultPage(): JSX.Element { setText(aICompat?.compat?.body || "Loading..."); return aICompat.compat; - }, [api, rightUser, categoryId, birthdate]); + }, [api, rightUser, categoryId, birthdate, token]); useApiCall(loadData); diff --git a/src/components/HomePage/index.tsx b/src/components/HomePage/index.tsx index 6faf788..ea935ab 100644 --- a/src/components/HomePage/index.tsx +++ b/src/components/HomePage/index.tsx @@ -34,8 +34,7 @@ const buttonTextFormatter = (text: string): JSX.Element => { }; function HomePage(): JSX.Element { - const token = - "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjIzNjEyLCJpYXQiOjE2OTM0MTg5MTAsImV4cCI6MTcwMjA1ODkxMCwianRpIjoiNzg5MjkwYWItODg0YS00MGUyLTkyNjEtOWI2OGEyNjkwNmE0IiwiZW1haWwiOiJvdGhlckBleGFtcGxlLmNvbSIsInN0YXRlIjoicHJvdmVuIiwibG9jIjoiZW4iLCJ0eiI6LTI4ODAwLCJ0eXBlIjoiZW1haWwiLCJpc3MiOiJjb20ubGlmZS5hdXJhIn0.J2ocWIv5jKzuKMcwMgWMiNMyGg5qLlMAeln-bQm_9lw"; + const token = useSelector(selectors.selectToken) const { t } = useTranslation(); const navigate = useNavigate(); const dispatch = useDispatch(); diff --git a/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx b/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx index c9daef3..2487d31 100644 --- a/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx +++ b/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx @@ -23,20 +23,15 @@ export default function CheckoutForm() { setIsProcessing(true); - const { error, paymentIntent } = await stripe.confirmPayment({ + const { error } = await stripe.confirmPayment({ elements, confirmParams: { - return_url: window.location.href, - }, - redirect: "if_required", + return_url: `https://${window.location.host}/payment/result`, + } }); if (error) { setMessage(error?.message || "Oops! Something went wrong."); - } else if (paymentIntent && paymentIntent.status === "succeeded") { - setMessage("Payment succeeded!"); - } else { - setMessage("Unexpected state"); } setIsProcessing(false); diff --git a/src/components/PaymentPage/methods/Stripe/Modal.tsx b/src/components/PaymentPage/methods/Stripe/Modal.tsx index 6e19d6f..daa0eea 100644 --- a/src/components/PaymentPage/methods/Stripe/Modal.tsx +++ b/src/components/PaymentPage/methods/Stripe/Modal.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from "react"; import { Stripe, loadStripe } from "@stripe/stripe-js"; import { Elements } from "@stripe/react-stripe-js"; import CheckoutForm from "./CheckoutForm"; +import { useAuth } from "@/auth"; interface StripeModalProps { open: boolean; @@ -16,14 +17,15 @@ interface StripeModalProps { export function StripeModal({ open, onClose, -// onSuccess, +}: // onSuccess, // onError, -}: StripeModalProps): JSX.Element { +StripeModalProps): JSX.Element { const api = useApi(); + const { token } = useAuth(); const [stripePromise, setStripePromise] = useState | null>(null); const [clientSecret, setClientSecret] = useState(""); - const isLoading = false; + const [isLoading, setIsLoading ] = useState(true); useEffect(() => { (async () => { @@ -33,25 +35,18 @@ export function StripeModal({ }, [api]); useEffect(() => { - fetch("https://aura.wit.life/api/v1/user/subscription_receipts.json", { - method: "POST", - headers: { - Authorization: - "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjIzNjEyLCJpYXQiOjE2OTM0MTg5MTAsImV4cCI6MTcwMjA1ODkxMCwianRpIjoiNzg5MjkwYWItODg0YS00MGUyLTkyNjEtOWI2OGEyNjkwNmE0IiwiZW1haWwiOiJvdGhlckBleGFtcGxlLmNvbSIsInN0YXRlIjoicHJvdmVuIiwibG9jIjoiZW4iLCJ0eiI6LTI4ODAwLCJ0eXBlIjoiZW1haWwiLCJpc3MiOiJjb20ubGlmZS5hdXJhIn0.J2ocWIv5jKzuKMcwMgWMiNMyGg5qLlMAeln-bQm_9lw", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - way: "stripe", - subscription_receipt: { - item_interval: "year", - }, - }), - }).then(async (res) => { - const { subscription_receipt } = await res.json(); + if (!open) return; + (async () => { + + const { subscription_receipt } = await api.createSubscriptionReceipt({ + token, + itemInterval: "year", + }); const { client_secret } = subscription_receipt.data; setClientSecret(client_secret); - }); - }, []); + setIsLoading(false); + })(); + }, [api, token, open]); const handleClose = () => { onClose(); diff --git a/src/components/PaymentPage/results/ErrorPage/index.tsx b/src/components/PaymentPage/results/ErrorPage/index.tsx new file mode 100644 index 0000000..b5eab29 --- /dev/null +++ b/src/components/PaymentPage/results/ErrorPage/index.tsx @@ -0,0 +1,30 @@ +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import routes from "@/routes"; +import styles from "./styles.module.css"; +import Title from "@/components/Title"; +import MainButton from "@/components/MainButton"; + +function PaymentFailPage(): JSX.Element { + const { t } = useTranslation(); + const navigate = useNavigate(); + const handleNext = () => navigate(routes.client.paymentMethod()); + + return ( +
+ Exclamation Icon +
+ {t("auweb.pay_bad.title")} +

{t("auweb.pay_bad.text1")}

+
+
+

{t("auweb.pay_bad.text2")}

+ + {t("auweb.pay_bad.button")} + +
+
+ ); +} + +export default PaymentFailPage; diff --git a/src/components/PaymentPage/results/ErrorPage/styles.module.css b/src/components/PaymentPage/results/ErrorPage/styles.module.css new file mode 100644 index 0000000..fa479f4 --- /dev/null +++ b/src/components/PaymentPage/results/ErrorPage/styles.module.css @@ -0,0 +1,38 @@ +.page { + position: relative; + height: calc(100vh - 50px); + /* max-height: -webkit-fill-available; */ + overflow-y: scroll; + justify-content: center; + gap: 80px; +} + +.text { + display: flex; + flex-direction: column; +} + +.list { + font-weight: 500; + white-space: pre-wrap; + text-align: left; +} + +.description { + font-weight: 500; + text-align: center; +} + +.button { + width: 100%; + max-width: 260px; + border-radius: 50px; + background-color: #fe2b57; +} + +.bottom { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} diff --git a/src/components/PaymentPage/results/SuccessPage/index.tsx b/src/components/PaymentPage/results/SuccessPage/index.tsx new file mode 100644 index 0000000..ba9aed4 --- /dev/null +++ b/src/components/PaymentPage/results/SuccessPage/index.tsx @@ -0,0 +1,25 @@ +import { useNavigate } from 'react-router-dom' +import { useTranslation } from "react-i18next"; +import routes from '@/routes' +import styles from "./styles.module.css"; +import Title from "@/components/Title"; +import MainButton from "@/components/MainButton"; + +function PaymentSuccessPage(): JSX.Element { + const { t } = useTranslation(); + const navigate = useNavigate() + const handleNext = () => navigate(routes.client.home()) + + return ( +
+ Success Icon +
+ {t("auweb.pay_good.title")} +

{t("auweb.pay_good.text1")}

+
+ {t("auweb.pay_good.button")} +
+ ); +} + +export default PaymentSuccessPage; diff --git a/src/components/PaymentPage/results/SuccessPage/styles.module.css b/src/components/PaymentPage/results/SuccessPage/styles.module.css new file mode 100644 index 0000000..0c0efb9 --- /dev/null +++ b/src/components/PaymentPage/results/SuccessPage/styles.module.css @@ -0,0 +1,25 @@ +.page { + position: relative; + flex: auto; + height: calc(100vh - 50px); + max-height: -webkit-fill-available; + justify-content: center; + gap: 80px; +} + +.text { + display: flex; + flex-direction: column; +} + +.text > p { + text-align: center; + font-weight: 500; +} + +.button { + width: 100%; + max-width: 260px; + border-radius: 50px; + background-color: #FE2B57; +} \ No newline at end of file diff --git a/src/components/PaymentPage/results/index.tsx b/src/components/PaymentPage/results/index.tsx new file mode 100644 index 0000000..e9356e8 --- /dev/null +++ b/src/components/PaymentPage/results/index.tsx @@ -0,0 +1,21 @@ +import { useNavigate } from "react-router-dom"; +import routes from "@/routes"; +import { useEffect } from "react"; +import { useSearchParams } from "react-router-dom"; + +function PaymentResultPage(): JSX.Element { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const status = searchParams.get("redirect_status"); + + useEffect(() => { + if (status === "succeeded") { + return navigate(routes.client.paymentSuccess()); + } + return navigate(routes.client.paymentFail()); + }, [navigate, status]); + + return <>; +} + +export default PaymentResultPage; diff --git a/src/components/WallpaperPage/index.tsx b/src/components/WallpaperPage/index.tsx index e917a13..3719670 100644 --- a/src/components/WallpaperPage/index.tsx +++ b/src/components/WallpaperPage/index.tsx @@ -58,8 +58,7 @@ function WallpaperPage(): JSX.Element { const api = useApi(); const { i18n } = useTranslation(); const locale = i18n.language; - const token = - "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjIzNjEyLCJpYXQiOjE2OTM0MTg5MTAsImV4cCI6MTcwMjA1ODkxMCwianRpIjoiNzg5MjkwYWItODg0YS00MGUyLTkyNjEtOWI2OGEyNjkwNmE0IiwiZW1haWwiOiJvdGhlckBleGFtcGxlLmNvbSIsInN0YXRlIjoicHJvdmVuIiwibG9jIjoiZW4iLCJ0eiI6LTI4ODAwLCJ0eXBlIjoiZW1haWwiLCJpc3MiOiJjb20ubGlmZS5hdXJhIn0.J2ocWIv5jKzuKMcwMgWMiNMyGg5qLlMAeln-bQm_9lw"; + const token = useSelector(selectors.selectToken) const { user, diff --git a/src/routes.ts b/src/routes.ts index 6c47850..fd689fa 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -17,6 +17,9 @@ const routes = { attention: () => [host, "attention"].join("/"), feedback: () => [host, "feedback"].join("/"), paymentMethod: () => [host, "payment", "method"].join("/"), + paymentResult: () => [host, "payment", "result"].join("/"), + paymentSuccess: () => [host, "payment", "success"].join("/"), + paymentFail: () => [host, "payment", "fail"].join("/"), wallpaper: () => [host, "wallpaper"].join("/"), static: () => [host, "static", ":typeId"].join("/"), legal: (type: string) => [host, "static", type].join("/"), @@ -115,6 +118,9 @@ export const withoutFooterRoutes = [ routes.client.compatibilityResult(), routes.client.home(), routes.client.breathResult(), + routes.client.paymentResult(), + routes.client.paymentSuccess(), + routes.client.paymentFail(), ]; export const hasNoFooter = (path: string) => !withoutFooterRoutes.includes(path); @@ -134,6 +140,9 @@ export const withoutHeaderRoutes = [ routes.client.compatibility(), routes.client.subscription(), routes.client.paymentMethod(), + routes.client.paymentResult(), + routes.client.paymentSuccess(), + routes.client.paymentFail(), ]; export const hasNoHeader = (path: string) => !withoutHeaderRoutes.includes(path);