feat: add success and fail payment pages, fix payment system

This commit is contained in:
gofnnp 2023-10-22 04:16:48 +04:00
parent 237dec5681
commit 1fc35e973d
16 changed files with 279 additions and 92 deletions

BIN
public/ExclamationIcon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
public/SuccessIcon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

View File

@ -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<boolean>(false);
const [leoApng, setLeoApng] = useState<Error | APNG>(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<Asset[]>(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={<SubscriptionPage />}
/>
<Route path={routes.client.paymentMethod()} element={<PaymentPage />} />
<Route path={routes.client.paymentResult()} element={<PaymentResultPage />} />
<Route path={routes.client.paymentSuccess()} element={<PaymentSuccessPage />} />
<Route path={routes.client.paymentFail()} element={<PaymentFailPage />} />
<Route
path={routes.client.wallpaper()}
element={<ProtectWallpaperPage />}

View File

@ -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<UserCallbacks.IUserCallbacks>(createCallback);

View File

@ -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<AICompats.ICompat | AIRequests.IAiRequest>(loadData);

View File

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

View File

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

View File

@ -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<Promise<Stripe | null> | null>(null);
const [clientSecret, setClientSecret] = useState<string>("");
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();

View File

@ -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 (
<section className={`${styles.page} page`}>
<img src="/ExclamationIcon.png" alt="Exclamation Icon" />
<div className={styles.text}>
<Title variant="h1">{t("auweb.pay_bad.title")}</Title>
<p className={styles.list}>{t("auweb.pay_bad.text1")}</p>
</div>
<div className={styles.bottom}>
<p className={styles.description}>{t("auweb.pay_bad.text2")}</p>
<MainButton className={styles.button} onClick={handleNext}>
{t("auweb.pay_bad.button")}
</MainButton>
</div>
</section>
);
}
export default PaymentFailPage;

View File

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

View File

@ -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 (
<section className={`${styles.page} page`}>
<img src="/SuccessIcon.png" alt="Success Icon" />
<div className={styles.text}>
<Title variant="h1">{t("auweb.pay_good.title")}</Title>
<p>{t("auweb.pay_good.text1")}</p>
</div>
<MainButton className={styles.button} onClick={handleNext}>{t("auweb.pay_good.button")}</MainButton>
</section>
);
}
export default PaymentSuccessPage;

View File

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

View File

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

View File

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

View File

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