Merge branch 'develop' into 'main'
Develop See merge request witapp/aura-webapp!176
This commit is contained in:
commit
536bfb2920
BIN
public/amazon-pay-mark.png
Normal file
BIN
public/amazon-pay-mark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
public/google-pay-mark.png
Normal file
BIN
public/google-pay-mark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
public/link-pay-mark.png
Normal file
BIN
public/link-pay-mark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@ -171,6 +171,8 @@ export interface ICreateAuthorizePayload {
|
|||||||
source: ESourceAuthorization;
|
source: ESourceAuthorization;
|
||||||
profile?: Partial<ICreateAuthorizeUser>;
|
profile?: Partial<ICreateAuthorizeUser>;
|
||||||
partner?: Partial<Exclude<ICreateAuthorizeUser, "relationship_status">>;
|
partner?: Partial<Exclude<ICreateAuthorizeUser, "relationship_status">>;
|
||||||
|
sign?: boolean;
|
||||||
|
signDate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreateAuthorizeResponse {
|
export interface ICreateAuthorizeResponse {
|
||||||
|
|||||||
@ -118,12 +118,6 @@ function EmailEnterPage({
|
|||||||
Enter your email
|
Enter your email
|
||||||
</Title>
|
</Title>
|
||||||
<p className={styles["not-share"]}>{t("we_dont_share")}</p>
|
<p className={styles["not-share"]}>{t("we_dont_share")}</p>
|
||||||
<NameInput
|
|
||||||
value={name}
|
|
||||||
placeholder="Your name"
|
|
||||||
onValid={handleValidName}
|
|
||||||
onInvalid={() => setIsValidName(!isRequiredName)}
|
|
||||||
/>
|
|
||||||
<EmailInput
|
<EmailInput
|
||||||
name="email"
|
name="email"
|
||||||
value={email}
|
value={email}
|
||||||
@ -131,6 +125,12 @@ function EmailEnterPage({
|
|||||||
onValid={handleValidEmail}
|
onValid={handleValidEmail}
|
||||||
onInvalid={() => setIsValidEmail(false)}
|
onInvalid={() => setIsValidEmail(false)}
|
||||||
/>
|
/>
|
||||||
|
<NameInput
|
||||||
|
value={name}
|
||||||
|
placeholder="Your name"
|
||||||
|
onValid={handleValidName}
|
||||||
|
onInvalid={() => setIsValidName(!isRequiredName)}
|
||||||
|
/>
|
||||||
<Policy sizing="medium" className={styles.policy}>
|
<Policy sizing="medium" className={styles.policy}>
|
||||||
{t("_continue_agree", {
|
{t("_continue_agree", {
|
||||||
eulaLink: (
|
eulaLink: (
|
||||||
|
|||||||
@ -343,7 +343,7 @@ function HomePage(): JSX.Element {
|
|||||||
<Slider>
|
<Slider>
|
||||||
{predictionMoonsPeriods.map((item, index) => (
|
{predictionMoonsPeriods.map((item, index) => (
|
||||||
<NameHoroscopeSlider
|
<NameHoroscopeSlider
|
||||||
data={{ ...item, name: user.username || "I Am" }}
|
data={{ ...item, name: user.username || "" }}
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleNameHoroscope(item);
|
handleNameHoroscope(item);
|
||||||
|
|||||||
@ -1,121 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
PaymentRequestButtonElement,
|
|
||||||
useStripe,
|
|
||||||
useElements,
|
|
||||||
} from "@stripe/react-stripe-js";
|
|
||||||
import { PaymentRequest } from "@stripe/stripe-js";
|
|
||||||
import styles from "./styles.module.css";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import routes from "@/routes";
|
|
||||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
|
||||||
|
|
||||||
interface ApplePayButtonProps {
|
|
||||||
activeProduct: IPaywallProduct | null;
|
|
||||||
client_secret: string;
|
|
||||||
subscriptionReceiptId?: string;
|
|
||||||
returnUrl?: string;
|
|
||||||
setCanMakePayment?: (isCanMakePayment: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ApplePayButton({
|
|
||||||
activeProduct,
|
|
||||||
client_secret,
|
|
||||||
subscriptionReceiptId,
|
|
||||||
returnUrl,
|
|
||||||
setCanMakePayment,
|
|
||||||
}: ApplePayButtonProps) {
|
|
||||||
const stripe = useStripe();
|
|
||||||
const elements = useElements();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const getAmountFromProduct = (subPlan: IPaywallProduct) => {
|
|
||||||
if (subPlan.isTrial) {
|
|
||||||
return subPlan.trialPrice;
|
|
||||||
}
|
|
||||||
return subPlan.price;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!stripe || !elements || !activeProduct) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pr = stripe.paymentRequest({
|
|
||||||
country: "US",
|
|
||||||
currency: "usd",
|
|
||||||
total: {
|
|
||||||
label: activeProduct.name || "Subscription",
|
|
||||||
amount: getAmountFromProduct(activeProduct),
|
|
||||||
},
|
|
||||||
requestPayerName: true,
|
|
||||||
requestPayerEmail: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
pr.canMakePayment().then((result) => {
|
|
||||||
if (result) {
|
|
||||||
setPaymentRequest(pr);
|
|
||||||
setCanMakePayment?.(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
pr.on("paymentmethod", async (e) => {
|
|
||||||
const { error: stripeError, paymentIntent } =
|
|
||||||
await stripe.confirmCardPayment(
|
|
||||||
client_secret,
|
|
||||||
{
|
|
||||||
payment_method: e.paymentMethod.id,
|
|
||||||
},
|
|
||||||
{ handleActions: false }
|
|
||||||
);
|
|
||||||
paymentIntent;
|
|
||||||
|
|
||||||
if (stripeError) {
|
|
||||||
// Show error to your customer (e.g., insufficient funds)
|
|
||||||
navigate(
|
|
||||||
`${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=failed`
|
|
||||||
);
|
|
||||||
return e.complete("fail");
|
|
||||||
}
|
|
||||||
navigate(
|
|
||||||
returnUrl ||
|
|
||||||
`${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=succeeded`
|
|
||||||
);
|
|
||||||
e.complete("success");
|
|
||||||
// Show a success message to your customer
|
|
||||||
// There's a risk of the customer closing the window before callback
|
|
||||||
// execution. Set up a webhook or plugin to listen for the
|
|
||||||
// payment_intent.succeeded event that handles any business critical
|
|
||||||
// post-payment actions.
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [
|
|
||||||
client_secret,
|
|
||||||
dispatch,
|
|
||||||
elements,
|
|
||||||
navigate,
|
|
||||||
stripe,
|
|
||||||
subscriptionReceiptId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{paymentRequest && (
|
|
||||||
<PaymentRequestButtonElement
|
|
||||||
className={styles["stripe-element"]}
|
|
||||||
options={{
|
|
||||||
paymentRequest,
|
|
||||||
style: { paymentRequestButton: { height: "60px" } },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ApplePayButton;
|
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
import {
|
||||||
|
useStripe,
|
||||||
|
useElements,
|
||||||
|
ExpressCheckoutElement,
|
||||||
|
} from "@stripe/react-stripe-js";
|
||||||
|
import {
|
||||||
|
AvailablePaymentMethods,
|
||||||
|
StripeExpressCheckoutElementReadyEvent,
|
||||||
|
} from "@stripe/stripe-js";
|
||||||
|
import { checkExpressCheckoutStripeFormAvailable } from "@/data/paymentMethods";
|
||||||
|
|
||||||
|
interface IExpressCheckoutStripeProps {
|
||||||
|
clientSecret: string;
|
||||||
|
returnUrl?: string;
|
||||||
|
isHide?: boolean;
|
||||||
|
onAvailable?: (
|
||||||
|
isAvailable: boolean,
|
||||||
|
availableMethods: AvailablePaymentMethods | undefined
|
||||||
|
) => void;
|
||||||
|
onChangeLoading?: (isLoading: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExpressCheckoutStripe({
|
||||||
|
clientSecret,
|
||||||
|
returnUrl = "https://${window.location.host}/payment/result/",
|
||||||
|
isHide = false,
|
||||||
|
onAvailable,
|
||||||
|
onChangeLoading,
|
||||||
|
}: IExpressCheckoutStripeProps) {
|
||||||
|
const stripe = useStripe();
|
||||||
|
const elements = useElements();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
|
const [isAvailable, setIsAvailable] = useState(false);
|
||||||
|
const isHideForm = useMemo(
|
||||||
|
() => isHide || !isAvailable,
|
||||||
|
[isAvailable, isHide]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onConfirm = async () =>
|
||||||
|
// event: StripeExpressCheckoutElementConfirmEvent
|
||||||
|
{
|
||||||
|
if (!stripe || !elements) {
|
||||||
|
// Stripe.js hasn't loaded yet.
|
||||||
|
// Make sure to disable form submission until Stripe.js has loaded.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error: submitError } = await elements.submit();
|
||||||
|
if (submitError) {
|
||||||
|
setErrorMessage(submitError.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Create the PaymentIntent and obtain clientSecret
|
||||||
|
// const res = await fetch("/create-intent", {
|
||||||
|
// method: "POST",
|
||||||
|
// });
|
||||||
|
// const { client_secret: clientSecret } = await res.json();
|
||||||
|
|
||||||
|
// Confirm the PaymentIntent using the details collected by the Express Checkout Element
|
||||||
|
const { error } = await stripe.confirmPayment({
|
||||||
|
// `elements` instance used to create the Express Checkout Element
|
||||||
|
elements,
|
||||||
|
// `clientSecret` from the created PaymentIntent
|
||||||
|
clientSecret,
|
||||||
|
confirmParams: {
|
||||||
|
return_url: returnUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// This point is only reached if there's an immediate error when
|
||||||
|
// confirming the payment. Show the error to your customer (for example, payment details incomplete)
|
||||||
|
setErrorMessage(error.message);
|
||||||
|
} else {
|
||||||
|
// The payment UI automatically closes with a success animation.
|
||||||
|
// Your customer is redirected to your `return_url`.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onReady = (event: StripeExpressCheckoutElementReadyEvent) => {
|
||||||
|
const _isAvailable = checkExpressCheckoutStripeFormAvailable(
|
||||||
|
event.availablePaymentMethods
|
||||||
|
);
|
||||||
|
setIsAvailable(_isAvailable);
|
||||||
|
onAvailable && onAvailable(_isAvailable, event.availablePaymentMethods);
|
||||||
|
onChangeLoading && onChangeLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.container} ${isHideForm ? styles.hide : ""}`}>
|
||||||
|
<ExpressCheckoutElement
|
||||||
|
onReady={onReady}
|
||||||
|
onLoadError={() => onChangeLoading && onChangeLoading(false)}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
options={{
|
||||||
|
layout: {
|
||||||
|
maxColumns: 1,
|
||||||
|
overflow: "never",
|
||||||
|
},
|
||||||
|
paymentMethodOrder: ["apple_pay", "google_pay", "amazon_pay", "link"],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errorMessage && <p className={styles.error}>{errorMessage}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExpressCheckoutStripe;
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
height: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
width: 100%;
|
||||||
|
color: #FF5758;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||||
|
import { useCanUseStripeButton } from "@/hooks/payment/useCanUseStripeButton";
|
||||||
|
import { actions } from "@/store";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
|
||||||
|
interface ICheckAvailableStripeButtonProps {
|
||||||
|
activeProduct: IPaywallProduct | null;
|
||||||
|
clientSecret: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CheckAvailableStripeButton({
|
||||||
|
activeProduct,
|
||||||
|
clientSecret,
|
||||||
|
}: ICheckAvailableStripeButtonProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { paymentRequest, availableMethods } = useCanUseStripeButton({
|
||||||
|
activeProduct,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (paymentRequest && availableMethods) {
|
||||||
|
dispatch(
|
||||||
|
actions.payment.updateStripeButton({ paymentRequest, availableMethods })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [availableMethods, dispatch, paymentRequest]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CheckAvailableStripeButton;
|
||||||
27
src/components/PaymentPage/methods/StripeButton/index.tsx
Normal file
27
src/components/PaymentPage/methods/StripeButton/index.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { PaymentRequestButtonElement } from "@stripe/react-stripe-js";
|
||||||
|
import { CanMakePaymentResult, PaymentRequest } from "@stripe/stripe-js";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export type TCanMakePaymentResult = CanMakePaymentResult | null;
|
||||||
|
|
||||||
|
interface ApplePayButtonProps {
|
||||||
|
paymentRequest: PaymentRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StripeButton({ paymentRequest }: ApplePayButtonProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{paymentRequest && (
|
||||||
|
<PaymentRequestButtonElement
|
||||||
|
className={styles["stripe-element"]}
|
||||||
|
options={{
|
||||||
|
paymentRequest,
|
||||||
|
style: { paymentRequestButton: { height: "60px" } },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StripeButton;
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import styles from "./styles.module.css";
|
|
||||||
import Title from "@/components/Title";
|
|
||||||
|
|
||||||
interface ITotalTodayProps {
|
|
||||||
total: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TotalToday({ total }: ITotalTodayProps): JSX.Element {
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<Title className={styles.text} variant="h3">{"Total today:"}</Title>
|
|
||||||
<Title className={styles.text} variant="h3">{total}</Title>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TotalToday;
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #e7f5ee;
|
|
||||||
border-radius: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #000;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import styles from "./styles.module.css";
|
|
||||||
import TotalToday from "./TotalToday";
|
|
||||||
import ApplePayButton from "../PaymentPage/methods/ApplePayButton";
|
|
||||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
|
||||||
|
|
||||||
interface ISubPlanInformationProps {
|
|
||||||
product: IPaywallProduct;
|
|
||||||
client_secret?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPrice = (product: IPaywallProduct): string => {
|
|
||||||
return `$${
|
|
||||||
(product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100
|
|
||||||
}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
function SubPlanInformation({
|
|
||||||
product,
|
|
||||||
client_secret,
|
|
||||||
}: ISubPlanInformationProps): JSX.Element {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<TotalToday total={getPrice(product)} />
|
|
||||||
{client_secret && (
|
|
||||||
<ApplePayButton activeProduct={product} client_secret={client_secret} />
|
|
||||||
)}
|
|
||||||
<p className={styles.description}>
|
|
||||||
{t("auweb.pay.information").replaceAll("%@", getPrice(product))}.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SubPlanInformation;
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 300px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #666666;
|
|
||||||
text-align: left;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 16px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-pal-button {
|
|
||||||
width: 100%;
|
|
||||||
height: 60px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: #ffc43a;
|
|
||||||
border-radius: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errors {
|
|
||||||
color: red;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface ICheckboxProps {
|
||||||
|
checked: boolean;
|
||||||
|
onChange: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Checkbox({ checked, onChange }: ICheckboxProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles["checkbox-wrapper-4"]}>
|
||||||
|
<input
|
||||||
|
checked={checked}
|
||||||
|
className={styles["inp-cbx"]}
|
||||||
|
id="morning"
|
||||||
|
type="checkbox"
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<label className={styles["cbx"]} htmlFor="morning">
|
||||||
|
<span>
|
||||||
|
<svg width="12px" height="10px">
|
||||||
|
<use xlinkHref="#check-4"></use>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<svg className={styles["inline-svg"]}>
|
||||||
|
<symbol id="check-4" viewBox="0 0 12 10">
|
||||||
|
<polyline points="1.5 6 4.5 9 10.5 1"></polyline>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Checkbox;
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
.checkbox-wrapper-4 * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.checkbox-wrapper-4 .cbx {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.checkbox-wrapper-4 .cbx span {
|
||||||
|
float: left;
|
||||||
|
vertical-align: middle;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
.checkbox-wrapper-4 .cbx span:first-child {
|
||||||
|
position: relative;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transform: scale(1);
|
||||||
|
border: 1px solid #484848;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 1px 1px rgba(0,16,75,0.05);
|
||||||
|
}
|
||||||
|
.checkbox-wrapper-4 .cbx span:first-child svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
fill: none;
|
||||||
|
stroke: #fff;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-dasharray: 16px;
|
||||||
|
stroke-dashoffset: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
transition-delay: 0.1s;
|
||||||
|
transform: translate3d(-50%, -50%, 0);
|
||||||
|
}
|
||||||
|
.checkbox-wrapper-4 .cbx span:last-child {
|
||||||
|
padding-left: 8px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
.checkbox-wrapper-4 .inp-cbx {
|
||||||
|
position: absolute;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.checkbox-wrapper-4 .inp-cbx:checked + .cbx span:first-child {
|
||||||
|
background: #07f;
|
||||||
|
border-color: #07f;
|
||||||
|
animation: wave-4 0.4s ease;
|
||||||
|
}
|
||||||
|
.checkbox-wrapper-4 .inp-cbx:checked + .cbx span:first-child svg {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
.checkbox-wrapper-4 .inline-svg {
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 640px) {
|
||||||
|
.checkbox-wrapper-4 .cbx {
|
||||||
|
width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-moz-keyframes wave-4 {
|
||||||
|
50% {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes wave-4 {
|
||||||
|
50% {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-o-keyframes wave-4 {
|
||||||
|
50% {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes wave-4 {
|
||||||
|
50% {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import Checkbox from "../Checkbox";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { actions, selectors } from "@/store";
|
||||||
|
|
||||||
|
interface IPrivacyPolicyProps {
|
||||||
|
containerClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PrivacyPolicy({ containerClassName = "" }: IPrivacyPolicyProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { checked } = useSelector(selectors.selectPrivacyPolicy);
|
||||||
|
|
||||||
|
const handleChange = () => {
|
||||||
|
dispatch(actions.privacyPolicy.updateChecked(!checked));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.container} ${containerClassName}`}>
|
||||||
|
<Checkbox checked={checked} onChange={handleChange} />
|
||||||
|
<p className={styles.text}>
|
||||||
|
I agree to the{" "}
|
||||||
|
<a
|
||||||
|
href="https://aura.wit.life/privacy"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Privacy Policy
|
||||||
|
</a>
|
||||||
|
,{" "}
|
||||||
|
<a
|
||||||
|
href="https://aura.wit.life/terms"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Terms of use
|
||||||
|
</a>{" "}
|
||||||
|
and to the use of cookies and tracking technologies, that require your
|
||||||
|
consent
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PrivacyPolicy;
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 125%;
|
||||||
|
color: #515151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text > a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
function ErrorIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="11" strokeWidth={1.5} stroke="#e12b2b" />
|
||||||
|
<rect x="11.4297" y="6.85645" width="1.5" height="8" fill="#e12b2b" />
|
||||||
|
<rect
|
||||||
|
x="11.4297"
|
||||||
|
y="16.5703"
|
||||||
|
width="1.4"
|
||||||
|
height="1.4"
|
||||||
|
rx="0.571429"
|
||||||
|
fill="#e12b2b"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorIcon;
|
||||||
28
src/components/pages/ABDesign/v1/components/Toast/index.tsx
Normal file
28
src/components/pages/ABDesign/v1/components/Toast/index.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ErrorIcon from "./ErrorIcon";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
interface IToastProps {
|
||||||
|
variant: "error";
|
||||||
|
children: React.ReactNode;
|
||||||
|
classNameContainer?: string;
|
||||||
|
classNameToast?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Toast({
|
||||||
|
variant,
|
||||||
|
children,
|
||||||
|
classNameContainer = "",
|
||||||
|
classNameToast = "",
|
||||||
|
}: IToastProps) {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.container} ${classNameContainer}`}>
|
||||||
|
<div className={`${styles.toast} ${styles[variant]} ${classNameToast}`}>
|
||||||
|
{variant === "error" && <ErrorIcon />}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Toast;
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
.toast {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 24px 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
|
animation: appearance .8s linear(0 0%, 0 1.8%, 0.01 3.6%, 0.08 10.03%, 0.15 14.25%, 0.2 14.34%, 0.31 14.14%, 0.41 17.21%, 0.49 19.04%, 0.58 20.56%, 0.66 22.07%, 0.76 23.87%, 0.84 26.07%, 0.93 28.04%, 1.03 31.14%, 1.09 37.31%, 1.09 44.28%, 1.02 49.41%, 0.96 55%, 0.98 64%, 0.99 74.4%, 1 86.4%, 1 100%);
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
background-color: #ffdcdc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes appearance {
|
||||||
|
0% {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -133,19 +133,19 @@ function EmailEnterPage({
|
|||||||
Enter your email
|
Enter your email
|
||||||
</Title>
|
</Title>
|
||||||
<p className={styles["not-share"]}>{t("we_dont_share")}</p>
|
<p className={styles["not-share"]}>{t("we_dont_share")}</p>
|
||||||
|
<EmailInput
|
||||||
|
name="email"
|
||||||
|
value={email}
|
||||||
|
placeholder={t("your_email")}
|
||||||
|
onValid={handleValidEmail}
|
||||||
|
onInvalid={() => setIsValidEmail(false)}
|
||||||
|
/>
|
||||||
<NameInput
|
<NameInput
|
||||||
value={name}
|
value={name}
|
||||||
placeholder="Your name"
|
placeholder="Your name"
|
||||||
onValid={handleValidName}
|
onValid={handleValidName}
|
||||||
onInvalid={() => setIsValidName(!isRequiredName)}
|
onInvalid={() => setIsValidName(!isRequiredName)}
|
||||||
/>
|
/>
|
||||||
<EmailInput
|
|
||||||
name="email"
|
|
||||||
value={email}
|
|
||||||
placeholder={t("your_email")}
|
|
||||||
onValid={handleValidEmail}
|
|
||||||
onInvalid={() => setIsValidEmail(false)}
|
|
||||||
/>
|
|
||||||
<QuestionnaireGreenButton
|
<QuestionnaireGreenButton
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|||||||
@ -3,14 +3,16 @@ import Title from "@/components/Title";
|
|||||||
import { Gender } from "@/data";
|
import { Gender } from "@/data";
|
||||||
import { EProductKeys } from "@/data/products";
|
import { EProductKeys } from "@/data/products";
|
||||||
import routes from "@/routes";
|
import routes from "@/routes";
|
||||||
import { actions } from "@/store";
|
import { actions, selectors } from "@/store";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import BackgroundTopBlob from "../../ui/BackgroundTopBlob";
|
import BackgroundTopBlob from "../../ui/BackgroundTopBlob";
|
||||||
import { useDynamicSize } from "@/hooks/useDynamicSize";
|
import { useDynamicSize } from "@/hooks/useDynamicSize";
|
||||||
import Header from "../../components/Header";
|
import Header from "../../components/Header";
|
||||||
import { genders } from "../../data/genders";
|
import { genders } from "../../data/genders";
|
||||||
|
import PrivacyPolicy from "../../components/PrivacyPolicy";
|
||||||
|
import Toast from "../../components/Toast";
|
||||||
|
|
||||||
interface IGenderPageProps {
|
interface IGenderPageProps {
|
||||||
productKey?: EProductKeys;
|
productKey?: EProductKeys;
|
||||||
@ -22,16 +24,26 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element {
|
|||||||
const { targetId } = useParams();
|
const { targetId } = useParams();
|
||||||
const { width: pageWidth, elementRef: pageRef } = useDynamicSize({});
|
const { width: pageWidth, elementRef: pageRef } = useDynamicSize({});
|
||||||
const [selectedGender, setSelectedGender] = useState<Gender | null>(null);
|
const [selectedGender, setSelectedGender] = useState<Gender | null>(null);
|
||||||
|
const { checked: privacyPolicyChecked } = useSelector(
|
||||||
|
selectors.selectPrivacyPolicy
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isShowTryApp = targetId === "i";
|
const isShowTryApp = targetId === "i";
|
||||||
dispatch(actions.userConfig.addIsShowTryApp(isShowTryApp));
|
dispatch(actions.userConfig.addIsShowTryApp(isShowTryApp));
|
||||||
}, [dispatch, targetId]);
|
}, [dispatch, targetId]);
|
||||||
|
|
||||||
const selectGender = async (gender: Gender) => {
|
useEffect(() => {
|
||||||
setSelectedGender(gender);
|
if (privacyPolicyChecked && selectedGender) {
|
||||||
|
handleNext();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [privacyPolicyChecked, selectedGender]);
|
||||||
|
|
||||||
|
const handleNext = async () => {
|
||||||
|
if (!selectedGender) return;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
dispatch(actions.questionnaire.update({ gender: gender.id }));
|
dispatch(actions.questionnaire.update({ gender: selectedGender.id }));
|
||||||
if (productKey === EProductKeys["moons.pdf.aura"]) {
|
if (productKey === EProductKeys["moons.pdf.aura"]) {
|
||||||
return navigate(routes.client.epeBirthdate());
|
return navigate(routes.client.epeBirthdate());
|
||||||
}
|
}
|
||||||
@ -41,6 +53,18 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element {
|
|||||||
navigate(`/v1/questionnaire/profile/flowChoice`);
|
navigate(`/v1/questionnaire/profile/flowChoice`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectGender = async (gender: Gender) => {
|
||||||
|
if (selectedGender?.id === gender.id) {
|
||||||
|
setSelectedGender(null);
|
||||||
|
} else {
|
||||||
|
setSelectedGender(gender);
|
||||||
|
}
|
||||||
|
if (!privacyPolicyChecked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleNext();
|
||||||
|
};
|
||||||
|
|
||||||
const getButtonBGColor = (gender: Gender): string => {
|
const getButtonBGColor = (gender: Gender): string => {
|
||||||
const { colorAssociation } = gender;
|
const { colorAssociation } = gender;
|
||||||
if (Array.isArray(colorAssociation)) {
|
if (Array.isArray(colorAssociation)) {
|
||||||
@ -114,6 +138,12 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<PrivacyPolicy containerClassName={styles["privacy-policy"]} />
|
||||||
|
{selectedGender && !privacyPolicyChecked && (
|
||||||
|
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||||
|
To continue, please accept our terms and policies
|
||||||
|
</Toast>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,18 +106,43 @@
|
|||||||
|
|
||||||
.gender--selected {
|
.gender--selected {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
/* animation: gender-click 1s linear; */
|
/* animation: gender-click 1.4s linear; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.gender--selected .gender__slide-element {
|
.gender--selected .gender__slide-element {
|
||||||
left: calc(100% - 27px - 10px);
|
left: calc(100% - 27px - 10px);
|
||||||
|
/* animation: gender-slide 1.4s linear; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-policy {
|
||||||
|
max-width: 316px;
|
||||||
|
margin-top: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-container {
|
||||||
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes gender-click {
|
@keyframes gender-click {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
100% {
|
50% {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gender-slide {
|
||||||
|
0% {
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
left: calc(100% - 27px - 10px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -4,9 +4,8 @@ import PaymentMethodsChoice from "../PaymentMethodsChoice";
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
|
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
|
||||||
import { Elements } from "@stripe/react-stripe-js";
|
import { Elements } from "@stripe/react-stripe-js";
|
||||||
import ApplePayButton from "@/components/PaymentPage/methods/ApplePayButton";
|
|
||||||
import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
|
import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
|
||||||
import { Stripe, loadStripe } from "@stripe/stripe-js";
|
import { AvailablePaymentMethods, Stripe, loadStripe } from "@stripe/stripe-js";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { selectors } from "@/store";
|
import { selectors } from "@/store";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@ -16,6 +15,7 @@ import SecurityPayments from "../SecurityPayments";
|
|||||||
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
||||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||||
import { useMakePayment } from "@/hooks/payment/useMakePayment";
|
import { useMakePayment } from "@/hooks/payment/useMakePayment";
|
||||||
|
import ExpressCheckoutStripe from "@/components/PaymentPage/methods/ExpressCheckoutStripe";
|
||||||
|
|
||||||
interface IPaymentModalProps {
|
interface IPaymentModalProps {
|
||||||
activeProduct?: IPaywallProduct;
|
activeProduct?: IPaywallProduct;
|
||||||
@ -41,14 +41,14 @@ function PaymentModal({
|
|||||||
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
|
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
|
||||||
const _activeProduct = activeProduct ? activeProduct : activeProductFromStore;
|
const _activeProduct = activeProduct ? activeProduct : activeProductFromStore;
|
||||||
const { products, paywallId, placementId } = usePaywall({ placementKey });
|
const { products, paywallId, placementId } = usePaywall({ placementKey });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
paymentIntentId,
|
paymentIntentId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
returnUrl: checkoutUrl,
|
returnUrl: checkoutUrl,
|
||||||
paymentType,
|
paymentType,
|
||||||
publicKey,
|
publicKey,
|
||||||
isLoading,
|
isLoading: isLoadingPayment,
|
||||||
error,
|
error,
|
||||||
} = useMakePayment({
|
} = useMakePayment({
|
||||||
productId: _activeProduct?._id || "",
|
productId: _activeProduct?._id || "",
|
||||||
@ -57,16 +57,27 @@ function PaymentModal({
|
|||||||
returnPaidUrl: returnUrl,
|
returnPaidUrl: returnUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [availableMethods, setAvailableMethods] = useState<
|
||||||
|
AvailablePaymentMethods | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
const [isLoadingExpressCheckout, setIsLoadingExpressCheckout] =
|
||||||
|
useState(true);
|
||||||
|
|
||||||
|
const isLoading = useMemo(() => {
|
||||||
|
return isLoadingPayment || isLoadingExpressCheckout;
|
||||||
|
}, [isLoadingPayment, isLoadingExpressCheckout]);
|
||||||
|
|
||||||
if (checkoutUrl?.length) {
|
if (checkoutUrl?.length) {
|
||||||
window.location.href = checkoutUrl;
|
window.location.href = checkoutUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentMethodsButtons = useMemo(() => {
|
const paymentMethodsButtons = useMemo(() => {
|
||||||
return paymentMethods;
|
return paymentMethods(availableMethods || null);
|
||||||
}, []);
|
}, [availableMethods]);
|
||||||
|
|
||||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
|
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
|
||||||
EPaymentMethod.PAYMENT_BUTTONS
|
paymentMethodsButtons[0].id
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSelectPaymentMethod = (method: EPaymentMethod) => {
|
const onSelectPaymentMethod = (method: EPaymentMethod) => {
|
||||||
@ -86,15 +97,16 @@ function PaymentModal({
|
|||||||
})();
|
})();
|
||||||
}, [_activeProduct, navigate, products, publicKey]);
|
}, [_activeProduct, navigate, products, publicKey]);
|
||||||
|
|
||||||
if (isLoading) {
|
const onAvailableExpressCheckout = (
|
||||||
return (
|
isAvailable: boolean,
|
||||||
<div className={styles["payment-modal"]}>
|
availableMethods: AvailablePaymentMethods | undefined
|
||||||
<div className={styles["payment-loader"]}>
|
) => {
|
||||||
<Loader />
|
if (isAvailable && availableMethods) {
|
||||||
</div>
|
setAvailableMethods(availableMethods);
|
||||||
</div>
|
return setSelectedPaymentMethod(EPaymentMethod.PAYMENT_BUTTONS);
|
||||||
);
|
}
|
||||||
}
|
return setAvailableMethods(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
if (error?.length) {
|
if (error?.length) {
|
||||||
return (
|
return (
|
||||||
@ -107,60 +119,75 @@ function PaymentModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["payment-modal"]}>
|
<>
|
||||||
<Title variant="h3" className={styles.title}>
|
{isLoading && (
|
||||||
Choose payment method
|
<div className={styles["payment-modal"]}>
|
||||||
</Title>
|
<div className={styles["payment-loader"]}>
|
||||||
<PaymentMethodsChoice
|
<Loader />
|
||||||
paymentMethods={paymentMethodsButtons}
|
</div>
|
||||||
selectedPaymentMethod={selectedPaymentMethod}
|
|
||||||
onSelectPaymentMethod={onSelectPaymentMethod}
|
|
||||||
/>
|
|
||||||
{_activeProduct && (
|
|
||||||
<div>
|
|
||||||
{!noTrial && (
|
|
||||||
<>
|
|
||||||
<p className={styles["sub-plan-description"]}>
|
|
||||||
You will be charged only{" "}
|
|
||||||
<b>${getPrice(_activeProduct)} for your 3-day trial.</b>
|
|
||||||
</p>
|
|
||||||
<p className={styles["sub-plan-description"]}>
|
|
||||||
We`ll <b>email you a reminder</b> before your trial period ends.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p className={styles["sub-plan-description"]}>
|
|
||||||
Cancel anytime. The charge will appear on your bill as witapps.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles["payment-method-container"]}>
|
<div
|
||||||
{stripePromise && clientSecret && (
|
className={`${styles["payment-modal"]} ${isLoading ? styles.hide : ""}`}
|
||||||
<Elements stripe={stripePromise} options={{ clientSecret }}>
|
>
|
||||||
{selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
|
<Title variant="h3" className={styles.title}>
|
||||||
<div className={styles["payment-method"]}>
|
Choose payment method
|
||||||
<ApplePayButton
|
</Title>
|
||||||
activeProduct={_activeProduct}
|
<PaymentMethodsChoice
|
||||||
client_secret={clientSecret}
|
paymentMethods={paymentMethodsButtons}
|
||||||
subscriptionReceiptId={paymentIntentId}
|
selectedPaymentMethod={selectedPaymentMethod}
|
||||||
/>
|
onSelectPaymentMethod={onSelectPaymentMethod}
|
||||||
</div>
|
/>
|
||||||
|
{_activeProduct && (
|
||||||
|
<div>
|
||||||
|
{!noTrial && (
|
||||||
|
<>
|
||||||
|
<p className={styles["sub-plan-description"]}>
|
||||||
|
You will be charged only{" "}
|
||||||
|
<b>${getPrice(_activeProduct)} for your 3-day trial.</b>
|
||||||
|
</p>
|
||||||
|
<p className={styles["sub-plan-description"]}>
|
||||||
|
We`ll <b>email you a reminder</b> before your trial period
|
||||||
|
ends.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
|
<p className={styles["sub-plan-description"]}>
|
||||||
<CheckoutForm
|
Cancel anytime. The charge will appear on your bill as witapps.
|
||||||
confirmType={paymentType}
|
</p>
|
||||||
subscriptionReceiptId={paymentIntentId}
|
</div>
|
||||||
returnUrl={returnUrl}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Elements>
|
|
||||||
)}
|
)}
|
||||||
|
<div className={styles["payment-method-container"]}>
|
||||||
|
{stripePromise && clientSecret && (
|
||||||
|
<Elements stripe={stripePromise} options={{ clientSecret }}>
|
||||||
|
<ExpressCheckoutStripe
|
||||||
|
clientSecret={clientSecret}
|
||||||
|
returnUrl={returnUrl}
|
||||||
|
isHide={
|
||||||
|
selectedPaymentMethod !== EPaymentMethod.PAYMENT_BUTTONS
|
||||||
|
}
|
||||||
|
onAvailable={(_isAvailable, _availableMethods) =>
|
||||||
|
onAvailableExpressCheckout(_isAvailable, _availableMethods)
|
||||||
|
}
|
||||||
|
onChangeLoading={(isLoading) =>
|
||||||
|
setIsLoadingExpressCheckout(isLoading)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
|
||||||
|
<CheckoutForm
|
||||||
|
confirmType={paymentType}
|
||||||
|
subscriptionReceiptId={paymentIntentId}
|
||||||
|
returnUrl={returnUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Elements>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<SecurityPayments />
|
||||||
|
<p className={styles.address}>1123 Rimer Dr Moraga, California 94556</p>
|
||||||
</div>
|
</div>
|
||||||
<SecurityPayments />
|
</>
|
||||||
<p className={styles.address}>500 N RAINBOW BLVD LAS VEGAS, NV 89107</p>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,12 @@
|
|||||||
color: #2f2e37;
|
color: #2f2e37;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.payment-modal.hide {
|
||||||
|
min-height: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@ -31,6 +37,7 @@
|
|||||||
|
|
||||||
.address {
|
.address {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.payment-method {
|
.payment-method {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import styles from "./styles.module.css"
|
|||||||
|
|
||||||
function PaymentAddress() {
|
function PaymentAddress() {
|
||||||
return (
|
return (
|
||||||
<p className={styles.address}>500 N RAINBOW BLVD LAS VEGAS, NV 89107</p>
|
<p className={styles.address}>1123 Rimer Dr Moraga, California 94556</p>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,4 +4,5 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 9px;
|
margin-top: 9px;
|
||||||
color: rgb(130, 130, 130);
|
color: rgb(130, 130, 130);
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ function PaymentForm({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<SecurityPayments />
|
<SecurityPayments />
|
||||||
<p className={styles.address}>500 N RAINBOW BLVD LAS VEGAS, NV 89107</p>
|
<p className={styles.address}>1123 Rimer Dr Moraga, California 94556</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,4 +41,5 @@
|
|||||||
.address {
|
.address {
|
||||||
color: gray;
|
color: gray;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,8 @@ import PaymentMethodsChoice from "../PaymentMethodsChoice";
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
|
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
|
||||||
import { Elements } from "@stripe/react-stripe-js";
|
import { Elements } from "@stripe/react-stripe-js";
|
||||||
import ApplePayButton from "@/components/PaymentPage/methods/ApplePayButton";
|
|
||||||
import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
|
import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
|
||||||
import { Stripe, loadStripe } from "@stripe/stripe-js";
|
import { AvailablePaymentMethods, Stripe, loadStripe } from "@stripe/stripe-js";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { selectors } from "@/store";
|
import { selectors } from "@/store";
|
||||||
import Loader from "@/components/Loader";
|
import Loader from "@/components/Loader";
|
||||||
@ -14,6 +13,9 @@ import SecurityPayments from "../SecurityPayments";
|
|||||||
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
|
||||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||||
import { useMakePayment } from "@/hooks/payment/useMakePayment";
|
import { useMakePayment } from "@/hooks/payment/useMakePayment";
|
||||||
|
import ExpressCheckoutStripe from "@/components/PaymentPage/methods/ExpressCheckoutStripe";
|
||||||
|
import routes from "@/routes";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
interface IPaymentModalProps {
|
interface IPaymentModalProps {
|
||||||
activeProduct?: IPaywallProduct;
|
activeProduct?: IPaywallProduct;
|
||||||
@ -35,6 +37,7 @@ function PaymentModal({
|
|||||||
returnUrl,
|
returnUrl,
|
||||||
placementKey = EPlacementKeys["aura.placement.main"],
|
placementKey = EPlacementKeys["aura.placement.main"],
|
||||||
}: IPaymentModalProps) {
|
}: IPaymentModalProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [stripePromise, setStripePromise] =
|
const [stripePromise, setStripePromise] =
|
||||||
useState<Promise<Stripe | null> | null>(null);
|
useState<Promise<Stripe | null> | null>(null);
|
||||||
|
|
||||||
@ -50,7 +53,7 @@ function PaymentModal({
|
|||||||
returnUrl: checkoutUrl,
|
returnUrl: checkoutUrl,
|
||||||
paymentType,
|
paymentType,
|
||||||
publicKey,
|
publicKey,
|
||||||
isLoading,
|
isLoading: isLoadingPayment,
|
||||||
error,
|
error,
|
||||||
} = useMakePayment({
|
} = useMakePayment({
|
||||||
productId: _activeProduct?._id || "",
|
productId: _activeProduct?._id || "",
|
||||||
@ -59,19 +62,27 @@ function PaymentModal({
|
|||||||
returnPaidUrl: returnUrl,
|
returnPaidUrl: returnUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [availableMethods, setAvailableMethods] = useState<
|
||||||
|
AvailablePaymentMethods | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
const [isLoadingExpressCheckout, setIsLoadingExpressCheckout] =
|
||||||
|
useState(true);
|
||||||
|
|
||||||
|
const isLoading = useMemo(() => {
|
||||||
|
return isLoadingPayment || isLoadingExpressCheckout;
|
||||||
|
}, [isLoadingPayment, isLoadingExpressCheckout]);
|
||||||
|
|
||||||
if (checkoutUrl?.length) {
|
if (checkoutUrl?.length) {
|
||||||
window.location.href = checkoutUrl;
|
window.location.href = checkoutUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentMethodsButtons = useMemo(() => {
|
const paymentMethodsButtons = useMemo(() => {
|
||||||
// return paymentMethods.filter(
|
return paymentMethods(availableMethods || null);
|
||||||
// (method) => method.id !== EPaymentMethod.PAYMENT_BUTTONS
|
}, [availableMethods]);
|
||||||
// );
|
|
||||||
return paymentMethods;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
|
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
|
||||||
EPaymentMethod.PAYMENT_BUTTONS
|
paymentMethodsButtons[0].id
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSelectPaymentMethod = (method: EPaymentMethod) => {
|
const onSelectPaymentMethod = (method: EPaymentMethod) => {
|
||||||
@ -82,18 +93,25 @@ function PaymentModal({
|
|||||||
(async () => {
|
(async () => {
|
||||||
if (!products?.length || !publicKey) return;
|
if (!products?.length || !publicKey) return;
|
||||||
setStripePromise(loadStripe(publicKey));
|
setStripePromise(loadStripe(publicKey));
|
||||||
|
const isActiveProduct = products.find(
|
||||||
|
(product) => product._id === _activeProduct?._id
|
||||||
|
);
|
||||||
|
if (!_activeProduct || !isActiveProduct) {
|
||||||
|
navigate(routes.client.trialChoice());
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
}, [products, publicKey]);
|
}, [_activeProduct, navigate, products, publicKey]);
|
||||||
|
|
||||||
if (isLoading) {
|
const onAvailableExpressCheckout = (
|
||||||
return (
|
isAvailable: boolean,
|
||||||
<div className={styles["payment-modal"]}>
|
availableMethods: AvailablePaymentMethods | undefined
|
||||||
<div className={styles["payment-loader"]}>
|
) => {
|
||||||
<Loader />
|
if (isAvailable && availableMethods) {
|
||||||
</div>
|
setAvailableMethods(availableMethods);
|
||||||
</div>
|
return setSelectedPaymentMethod(EPaymentMethod.PAYMENT_BUTTONS);
|
||||||
);
|
}
|
||||||
}
|
return setAvailableMethods(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
if (error?.length) {
|
if (error?.length) {
|
||||||
return (
|
return (
|
||||||
@ -106,60 +124,75 @@ function PaymentModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["payment-modal"]}>
|
<>
|
||||||
<Title variant="h3" className={styles.title}>
|
{isLoading && (
|
||||||
Choose payment method
|
<div className={styles["payment-modal"]}>
|
||||||
</Title>
|
<div className={styles["payment-loader"]}>
|
||||||
<PaymentMethodsChoice
|
<Loader />
|
||||||
paymentMethods={paymentMethodsButtons}
|
</div>
|
||||||
selectedPaymentMethod={selectedPaymentMethod}
|
|
||||||
onSelectPaymentMethod={onSelectPaymentMethod}
|
|
||||||
/>
|
|
||||||
{_activeProduct && (
|
|
||||||
<div>
|
|
||||||
{!noTrial && (
|
|
||||||
<>
|
|
||||||
<p className={styles["sub-plan-description"]}>
|
|
||||||
You will be charged only{" "}
|
|
||||||
<b>${getPrice(_activeProduct)} for your 3-day trial.</b>
|
|
||||||
</p>
|
|
||||||
<p className={styles["sub-plan-description"]}>
|
|
||||||
We`ll <b>email you a reminder</b> before your trial period ends.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p className={styles["sub-plan-description"]}>
|
|
||||||
Cancel anytime. The charge will appear on your bill as witapps.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles["payment-method-container"]}>
|
<div
|
||||||
{stripePromise && clientSecret && (
|
className={`${styles["payment-modal"]} ${isLoading ? styles.hide : ""}`}
|
||||||
<Elements stripe={stripePromise} options={{ clientSecret }}>
|
>
|
||||||
{selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
|
<Title variant="h3" className={styles.title}>
|
||||||
<div className={styles["payment-method"]}>
|
Choose payment method
|
||||||
<ApplePayButton
|
</Title>
|
||||||
activeProduct={_activeProduct}
|
<PaymentMethodsChoice
|
||||||
client_secret={clientSecret}
|
paymentMethods={paymentMethodsButtons}
|
||||||
subscriptionReceiptId={paymentIntentId}
|
selectedPaymentMethod={selectedPaymentMethod}
|
||||||
/>
|
onSelectPaymentMethod={onSelectPaymentMethod}
|
||||||
</div>
|
/>
|
||||||
|
{_activeProduct && (
|
||||||
|
<div>
|
||||||
|
{!noTrial && (
|
||||||
|
<>
|
||||||
|
<p className={styles["sub-plan-description"]}>
|
||||||
|
You will be charged only{" "}
|
||||||
|
<b>${getPrice(_activeProduct)} for your 3-day trial.</b>
|
||||||
|
</p>
|
||||||
|
<p className={styles["sub-plan-description"]}>
|
||||||
|
We`ll <b>email you a reminder</b> before your trial period
|
||||||
|
ends.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
|
<p className={styles["sub-plan-description"]}>
|
||||||
<CheckoutForm
|
Cancel anytime. The charge will appear on your bill as witapps.
|
||||||
confirmType={paymentType}
|
</p>
|
||||||
subscriptionReceiptId={paymentIntentId}
|
</div>
|
||||||
returnUrl={returnUrl}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Elements>
|
|
||||||
)}
|
)}
|
||||||
|
<div className={styles["payment-method-container"]}>
|
||||||
|
{stripePromise && clientSecret && (
|
||||||
|
<Elements stripe={stripePromise} options={{ clientSecret }}>
|
||||||
|
<ExpressCheckoutStripe
|
||||||
|
clientSecret={clientSecret}
|
||||||
|
returnUrl={returnUrl}
|
||||||
|
isHide={
|
||||||
|
selectedPaymentMethod !== EPaymentMethod.PAYMENT_BUTTONS
|
||||||
|
}
|
||||||
|
onAvailable={(_isAvailable, _availableMethods) =>
|
||||||
|
onAvailableExpressCheckout(_isAvailable, _availableMethods)
|
||||||
|
}
|
||||||
|
onChangeLoading={(isLoading) =>
|
||||||
|
setIsLoadingExpressCheckout(isLoading)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
|
||||||
|
<CheckoutForm
|
||||||
|
confirmType={paymentType}
|
||||||
|
subscriptionReceiptId={paymentIntentId}
|
||||||
|
returnUrl={returnUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Elements>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<SecurityPayments />
|
||||||
|
<p className={styles.address}>1123 Rimer Dr Moraga, California 94556</p>
|
||||||
</div>
|
</div>
|
||||||
<SecurityPayments />
|
</>
|
||||||
<p className={styles.address}>500 N RAINBOW BLVD LAS VEGAS, NV 89107</p>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,12 @@
|
|||||||
color: #2f2e37;
|
color: #2f2e37;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.payment-modal.hide {
|
||||||
|
min-height: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@ -31,6 +37,7 @@
|
|||||||
|
|
||||||
.address {
|
.address {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.payment-method {
|
.payment-method {
|
||||||
|
|||||||
@ -1,9 +1,33 @@
|
|||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
import { TCanMakePaymentResult } from "@/components/PaymentPage/methods/StripeButton";
|
||||||
|
|
||||||
function PaymentButtons() {
|
interface IPaymentButtonsProps {
|
||||||
return <div className={styles.container}>
|
availableMethods: TCanMakePaymentResult;
|
||||||
<img src="/applepay.webp" alt="ApplePay" />
|
}
|
||||||
</div>;
|
const Image = ({ availableMethods }: IPaymentButtonsProps) => {
|
||||||
|
if (!availableMethods) return <></>;
|
||||||
|
if (availableMethods["applePay"]) {
|
||||||
|
return <img src="/applepay.webp" alt="ApplePay" />;
|
||||||
|
}
|
||||||
|
if (availableMethods["googlePay"]) {
|
||||||
|
return <img src="/google-pay-mark.png" alt="google" />;
|
||||||
|
}
|
||||||
|
if (availableMethods["amazon"]) {
|
||||||
|
return <img src="/amazon-pay-mark.png" alt="AmazonPay" />;
|
||||||
|
}
|
||||||
|
if (availableMethods["link"]) {
|
||||||
|
return <img src="/link-pay-mark.png" alt="LinkPay" />;
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function PaymentButtons({ availableMethods }: IPaymentButtonsProps) {
|
||||||
|
if (!availableMethods) return <></>;
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Image availableMethods={availableMethods} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PaymentButtons;
|
export default PaymentButtons;
|
||||||
|
|||||||
@ -8,5 +8,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container > img {
|
.container > img {
|
||||||
height: 16px;
|
height: 22px;
|
||||||
}
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
|
import { TCanMakePaymentResult } from "@/components/PaymentPage/methods/StripeButton";
|
||||||
import CreditCard from "@/components/ui/PaymentMethodsButtons/CreditCard";
|
import CreditCard from "@/components/ui/PaymentMethodsButtons/CreditCard";
|
||||||
import PaymentButtons from "@/components/ui/PaymentMethodsButtons/PaymentButtons";
|
import PaymentButtons from "@/components/ui/PaymentMethodsButtons/PaymentButtons";
|
||||||
|
import { AvailablePaymentMethods } from "@stripe/stripe-js";
|
||||||
|
|
||||||
export enum EPaymentMethod {
|
export enum EPaymentMethod {
|
||||||
CREDIT_CARD = "card",
|
CREDIT_CARD = "card",
|
||||||
@ -11,13 +13,54 @@ export interface IPaymentMethod {
|
|||||||
component: JSX.Element;
|
component: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const paymentMethods: IPaymentMethod[] = [
|
// export const paymentMethods: IPaymentMethod[] = [
|
||||||
{
|
// {
|
||||||
id: EPaymentMethod.PAYMENT_BUTTONS,
|
// id: EPaymentMethod.PAYMENT_BUTTONS,
|
||||||
component: <PaymentButtons />,
|
// component: <PaymentButtons />,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: EPaymentMethod.CREDIT_CARD,
|
// id: EPaymentMethod.CREDIT_CARD,
|
||||||
component: <CreditCard />,
|
// component: <CreditCard />,
|
||||||
},
|
// },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
|
export const checkExpressCheckoutStripeFormAvailable = (
|
||||||
|
availablePaymentMethods: undefined | AvailablePaymentMethods
|
||||||
|
) => {
|
||||||
|
if (!availablePaymentMethods) return false;
|
||||||
|
let result = false;
|
||||||
|
for (const key in availablePaymentMethods) {
|
||||||
|
if (availablePaymentMethods[key as keyof AvailablePaymentMethods]) {
|
||||||
|
result = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function paymentMethods(
|
||||||
|
availableMethods: TCanMakePaymentResult
|
||||||
|
): IPaymentMethod[] {
|
||||||
|
let methods = [
|
||||||
|
{
|
||||||
|
id: EPaymentMethod.PAYMENT_BUTTONS,
|
||||||
|
component: <PaymentButtons availableMethods={availableMethods} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: EPaymentMethod.CREDIT_CARD,
|
||||||
|
component: <CreditCard />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!availableMethods ||
|
||||||
|
!checkExpressCheckoutStripeFormAvailable(
|
||||||
|
availableMethods as AvailablePaymentMethods
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
methods = methods.filter(
|
||||||
|
(method) => method.id !== EPaymentMethod.PAYMENT_BUTTONS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return methods;
|
||||||
|
}
|
||||||
|
|||||||
@ -33,6 +33,7 @@ export const useAuthentication = () => {
|
|||||||
partnerBirthPlace,
|
partnerBirthPlace,
|
||||||
partnerBirthtime,
|
partnerBirthtime,
|
||||||
} = useSelector(selectors.selectQuestionnaire)
|
} = useSelector(selectors.selectQuestionnaire)
|
||||||
|
const { checked, dateOfCheck } = useSelector(selectors.selectPrivacyPolicy)
|
||||||
|
|
||||||
const birthdateFromForm = useSelector(selectors.selectBirthdate);
|
const birthdateFromForm = useSelector(selectors.selectBirthdate);
|
||||||
const birthtimeFromForm = useSelector(selectors.selectBirthtime);
|
const birthtimeFromForm = useSelector(selectors.selectBirthtime);
|
||||||
@ -94,7 +95,9 @@ export const useAuthentication = () => {
|
|||||||
birthplace: {
|
birthplace: {
|
||||||
address: partnerBirthPlace,
|
address: partnerBirthPlace,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
sign: checked,
|
||||||
|
signDate: dateOfCheck
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [
|
||||||
@ -108,6 +111,8 @@ export const useAuthentication = () => {
|
|||||||
partnerName,
|
partnerName,
|
||||||
username,
|
username,
|
||||||
birthtime,
|
birthtime,
|
||||||
|
checked,
|
||||||
|
dateOfCheck
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const authorization = useCallback(async (email: string, source: ESourceAuthorization) => {
|
const authorization = useCallback(async (email: string, source: ESourceAuthorization) => {
|
||||||
|
|||||||
98
src/hooks/payment/useCanUseStripeButton.ts
Normal file
98
src/hooks/payment/useCanUseStripeButton.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||||
|
import routes from "@/routes";
|
||||||
|
import { useElements, useStripe } from "@stripe/react-stripe-js";
|
||||||
|
import { CanMakePaymentResult, PaymentRequest } from "@stripe/stripe-js";
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export type TCanMakePaymentResult = CanMakePaymentResult | null;
|
||||||
|
|
||||||
|
|
||||||
|
const getAmountFromProduct = (subPlan: IPaywallProduct) => {
|
||||||
|
if (subPlan.isTrial) {
|
||||||
|
return subPlan.trialPrice;
|
||||||
|
}
|
||||||
|
return subPlan.price;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IUseCanUseStripeButton {
|
||||||
|
activeProduct: IPaywallProduct | null;
|
||||||
|
client_secret: string;
|
||||||
|
subscriptionReceiptId?: string;
|
||||||
|
returnUrl?: string;
|
||||||
|
setCanMakePayment?: (canMakePayment: TCanMakePaymentResult) => void;
|
||||||
|
setCanMakePaymentLoading?: (loading: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCanUseStripeButton = ({
|
||||||
|
activeProduct,
|
||||||
|
client_secret,
|
||||||
|
subscriptionReceiptId,
|
||||||
|
returnUrl,
|
||||||
|
}: IUseCanUseStripeButton) => {
|
||||||
|
const stripe = useStripe();
|
||||||
|
const elements = useElements();
|
||||||
|
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [availableMethods, setAvailableMethods] = useState<TCanMakePaymentResult>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stripe || !elements || !activeProduct) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pr = stripe.paymentRequest({
|
||||||
|
country: "US",
|
||||||
|
currency: "usd",
|
||||||
|
total: {
|
||||||
|
label: activeProduct.name || "Subscription",
|
||||||
|
amount: getAmountFromProduct(activeProduct),
|
||||||
|
},
|
||||||
|
requestPayerName: true,
|
||||||
|
requestPayerEmail: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
pr.canMakePayment()
|
||||||
|
.then((result) => {
|
||||||
|
if (result) {
|
||||||
|
setPaymentRequest(pr)
|
||||||
|
setAvailableMethods(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
pr.on("paymentmethod", async (e) => {
|
||||||
|
const { error: stripeError, paymentIntent } =
|
||||||
|
await stripe.confirmCardPayment(
|
||||||
|
client_secret,
|
||||||
|
{
|
||||||
|
payment_method: e.paymentMethod.id,
|
||||||
|
},
|
||||||
|
{ handleActions: false }
|
||||||
|
);
|
||||||
|
paymentIntent;
|
||||||
|
|
||||||
|
if (stripeError) {
|
||||||
|
// Show error to your customer (e.g., insufficient funds)
|
||||||
|
navigate(
|
||||||
|
`${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=failed`
|
||||||
|
);
|
||||||
|
return e.complete("fail");
|
||||||
|
}
|
||||||
|
navigate(
|
||||||
|
returnUrl ||
|
||||||
|
`${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=succeeded`
|
||||||
|
);
|
||||||
|
e.complete("success");
|
||||||
|
});
|
||||||
|
}, [activeProduct, client_secret, elements, navigate, returnUrl, stripe, subscriptionReceiptId]);
|
||||||
|
|
||||||
|
return useMemo(() => ({
|
||||||
|
paymentRequest,
|
||||||
|
availableMethods
|
||||||
|
}), [
|
||||||
|
paymentRequest,
|
||||||
|
availableMethods
|
||||||
|
])
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ export default {
|
|||||||
next: "Next",
|
next: "Next",
|
||||||
date_of_birth: "What's your date of birth?",
|
date_of_birth: "What's your date of birth?",
|
||||||
privacy_text: "By continuing, you agree to our <eulaLink> and <privacyLink>. Have a question? Reach our support team <clickHere>",
|
privacy_text: "By continuing, you agree to our <eulaLink> and <privacyLink>. Have a question? Reach our support team <clickHere>",
|
||||||
eula: "Hint's EULA",
|
eula: "EULA",
|
||||||
here: 'here',
|
here: 'here',
|
||||||
privacy_notice: 'Privacy Notice',
|
privacy_notice: 'Privacy Notice',
|
||||||
born_time_question: "What time were you born?",
|
born_time_question: "What time were you born?",
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import payment, {
|
|||||||
actions as paymentActions,
|
actions as paymentActions,
|
||||||
selectActiveProduct,
|
selectActiveProduct,
|
||||||
selectIsDiscount,
|
selectIsDiscount,
|
||||||
|
selectStripeButton,
|
||||||
selectSubscriptionReceipt,
|
selectSubscriptionReceipt,
|
||||||
} from "./payment";
|
} from "./payment";
|
||||||
import subscriptionPlans, {
|
import subscriptionPlans, {
|
||||||
@ -69,6 +70,7 @@ import palmistry, {
|
|||||||
selectPalmistryLines,
|
selectPalmistryLines,
|
||||||
} from "./palmistry";
|
} from "./palmistry";
|
||||||
import { selectPaywallsIsMustUpdate, selectPaywalls } from "./paywalls";
|
import { selectPaywallsIsMustUpdate, selectPaywalls } from "./paywalls";
|
||||||
|
import privacyPolicy, { actions as privacyPolicyActions, selectPrivacyPolicy } from "./privacyPolicy";
|
||||||
|
|
||||||
const preloadedState = loadStore();
|
const preloadedState = loadStore();
|
||||||
export const actions = {
|
export const actions = {
|
||||||
@ -88,6 +90,7 @@ export const actions = {
|
|||||||
questionnaire: questionnaireActions,
|
questionnaire: questionnaireActions,
|
||||||
userConfig: userConfigActions,
|
userConfig: userConfigActions,
|
||||||
palmistry: palmistryActions,
|
palmistry: palmistryActions,
|
||||||
|
privacyPolicy: privacyPolicyActions,
|
||||||
reset: createAction("reset"),
|
reset: createAction("reset"),
|
||||||
};
|
};
|
||||||
export const selectors = {
|
export const selectors = {
|
||||||
@ -120,6 +123,8 @@ export const selectors = {
|
|||||||
selectPalmistryLines,
|
selectPalmistryLines,
|
||||||
selectPaywalls,
|
selectPaywalls,
|
||||||
selectPaywallsIsMustUpdate,
|
selectPaywallsIsMustUpdate,
|
||||||
|
selectPrivacyPolicy,
|
||||||
|
selectStripeButton,
|
||||||
...formSelectors,
|
...formSelectors,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -140,6 +145,7 @@ export const reducer = combineReducers({
|
|||||||
userConfig,
|
userConfig,
|
||||||
palmistry,
|
palmistry,
|
||||||
paywalls,
|
paywalls,
|
||||||
|
privacyPolicy
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RootState = ReturnType<typeof reducer>;
|
export type RootState = ReturnType<typeof reducer>;
|
||||||
|
|||||||
@ -1,13 +1,21 @@
|
|||||||
import { IPaywallProduct } from "@/api/resources/Paywall";
|
import { IPaywallProduct } from "@/api/resources/Paywall";
|
||||||
import { SubscriptionReceipt } from "@/api/resources/UserSubscriptionReceipts";
|
import { SubscriptionReceipt } from "@/api/resources/UserSubscriptionReceipts";
|
||||||
|
import { TCanMakePaymentResult } from "@/hooks/payment/useCanUseStripeButton";
|
||||||
import { createSlice, createSelector } from "@reduxjs/toolkit";
|
import { createSlice, createSelector } from "@reduxjs/toolkit";
|
||||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import { PaymentRequest } from "@stripe/stripe-js";
|
||||||
|
|
||||||
|
interface IStripeButton {
|
||||||
|
paymentRequest: PaymentRequest | null;
|
||||||
|
availableMethods: TCanMakePaymentResult;
|
||||||
|
}
|
||||||
|
|
||||||
interface IPayment {
|
interface IPayment {
|
||||||
selectedPrice: number | null;
|
selectedPrice: number | null;
|
||||||
isDiscount: boolean;
|
isDiscount: boolean;
|
||||||
subscriptionReceipt: SubscriptionReceipt | null;
|
subscriptionReceipt: SubscriptionReceipt | null;
|
||||||
activeProduct: IPaywallProduct | null;
|
activeProduct: IPaywallProduct | null;
|
||||||
|
stripeButton: IStripeButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: IPayment = {
|
const initialState: IPayment = {
|
||||||
@ -15,6 +23,10 @@ const initialState: IPayment = {
|
|||||||
isDiscount: false,
|
isDiscount: false,
|
||||||
subscriptionReceipt: null,
|
subscriptionReceipt: null,
|
||||||
activeProduct: null,
|
activeProduct: null,
|
||||||
|
stripeButton: {
|
||||||
|
paymentRequest: null,
|
||||||
|
availableMethods: null,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const paymentSlice = createSlice({
|
const paymentSlice = createSlice({
|
||||||
@ -24,6 +36,9 @@ const paymentSlice = createSlice({
|
|||||||
update(state, action: PayloadAction<Partial<IPayment>>) {
|
update(state, action: PayloadAction<Partial<IPayment>>) {
|
||||||
return { ...state, ...action.payload };
|
return { ...state, ...action.payload };
|
||||||
},
|
},
|
||||||
|
updateStripeButton(state, action: PayloadAction<IStripeButton>) {
|
||||||
|
return { ...state, stripeButton: action.payload };
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => builder.addCase("reset", () => initialState),
|
extraReducers: (builder) => builder.addCase("reset", () => initialState),
|
||||||
});
|
});
|
||||||
@ -45,4 +60,8 @@ export const selectSubscriptionReceipt = createSelector(
|
|||||||
(state: { payment: IPayment }) => state.payment.subscriptionReceipt,
|
(state: { payment: IPayment }) => state.payment.subscriptionReceipt,
|
||||||
(payment) => payment
|
(payment) => payment
|
||||||
);
|
);
|
||||||
|
export const selectStripeButton = createSelector(
|
||||||
|
(state: { payment: IPayment }) => state.payment.stripeButton,
|
||||||
|
(payment) => payment
|
||||||
|
);
|
||||||
export default paymentSlice.reducer;
|
export default paymentSlice.reducer;
|
||||||
|
|||||||
30
src/store/privacyPolicy.ts
Normal file
30
src/store/privacyPolicy.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { createSlice, createSelector } from '@reduxjs/toolkit'
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
|
interface IPrivacyPolicy {
|
||||||
|
checked: boolean
|
||||||
|
dateOfCheck: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: IPrivacyPolicy = {
|
||||||
|
checked: false,
|
||||||
|
dateOfCheck: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const privacyPolicySlice = createSlice({
|
||||||
|
name: 'privacyPolicy',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
updateChecked(state, action: PayloadAction<boolean>) {
|
||||||
|
return { ...state, checked: action.payload, dateOfCheck: new Date().toISOString() }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => builder.addCase('reset', () => initialState),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { actions } = privacyPolicySlice
|
||||||
|
export const selectPrivacyPolicy = createSelector(
|
||||||
|
(state: { privacyPolicy: IPrivacyPolicy }) => state.privacyPolicy,
|
||||||
|
(privacyPolicy) => privacyPolicy
|
||||||
|
)
|
||||||
|
export default privacyPolicySlice.reducer
|
||||||
@ -5,7 +5,7 @@ import { getClientLocale, getClientTimezone } from '../locales'
|
|||||||
|
|
||||||
const initialState: User.User = {
|
const initialState: User.User = {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
username: "I Am",
|
username: "",
|
||||||
email: '',
|
email: '',
|
||||||
locale: getClientLocale(),
|
locale: getClientLocale(),
|
||||||
state: '',
|
state: '',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user