feat: Change payment window on palmistry/payment

This commit is contained in:
yury 2024-06-25 04:24:00 +03:00
parent 55c2207a41
commit 5ca0a636cc
10 changed files with 486 additions and 188 deletions

View File

@ -0,0 +1,9 @@
export default function CreditCardIcon() {
return (
<svg width="23" height="16" viewBox="0 0 23 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" width="22" height="16" rx="2" fill="white"></rect>
<rect x="0.5" y="2.66406" width="22" height="2.66667" fill="#9FB8FF"></rect>
<rect x="3" y="7.35938" width="17" height="2" rx="1" fill="#CEDBFF"></rect>
</svg>
)
}

View File

@ -0,0 +1,39 @@
import { Elements } from '@stripe/react-stripe-js';
import CheckoutForm, { TConfirmType } from '@/components/PaymentPage/methods/CheckoutForm';
import Modal from '@/components/Modal';
import { Stripe } from '@stripe/stripe-js';
import { Dispatch, SetStateAction } from 'react';
import './style.scss';
interface IPaymentCardModalProps {
clientSecret?: string;
stripePromise: Promise<Stripe | null> | null;
paymentType?: TConfirmType;
paymentIntentId?: string;
returnUrl?: string;
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
}
export default function PaymentCardModal({
clientSecret,
stripePromise,
paymentType,
paymentIntentId,
returnUrl,
isOpen,
setIsOpen,
}: IPaymentCardModalProps) {
return (
<Modal open={isOpen} onClose={() => setIsOpen(false)}>
<Elements stripe={stripePromise} options={{clientSecret}}>
<CheckoutForm
confirmType={paymentType}
subscriptionReceiptId={paymentIntentId}
returnUrl={returnUrl}
/>
</Elements>
</Modal>
)
}

View File

@ -0,0 +1,7 @@
:global(.paymentCardModalContainer) {
background: none;
.p-PaymentMethodSelector {
display: none !important;
}
}

View File

@ -0,0 +1,155 @@
import Loader from '@/components/Loader';
import styles from './styles.module.scss';
import cn from 'classnames';
import { EPlacementKeys, IPaywallProduct } from '@/api/resources/Paywall';
import { useNavigate } from 'react-router-dom';
import { Dispatch, LegacyRef, SetStateAction, useEffect, useMemo, useRef, useState } from 'react';
import { loadStripe, Stripe } from '@stripe/stripe-js';
import { usePaywall } from '@/hooks/paywall/usePaywall';
import { useMakePayment } from '@/hooks/payment/useMakePayment';
import { getFormattedPrice } from '@/utils/price.utils';
import routes from '@/routes';
import Title from '@/components/Title';
import { Elements } from '@stripe/react-stripe-js';
import ExpressCheckoutStripe from '@/components/PaymentPage/methods/ExpressCheckoutStripe';
import SecurityPayments from '@/components/pages/TrialPayment/components/SecurityPayments';
import PaymentCardModal from '@/components/PaymentModalNew/PaymentCardModal';
import CreditCardIcon from '@/components/PaymentModalNew/PaymentCardModal/CreditCardIcon';
interface IPaymentModalNewProps {
returnUrl: string;
placementKey: EPlacementKeys;
activeProduct: IPaywallProduct;
setHeight?: Dispatch<SetStateAction<number>>;
}
export default function PaymentModalNew({
returnUrl,
activeProduct,
placementKey,
setHeight,
}: IPaymentModalNewProps) {
const navigate = useNavigate();
const ref = useRef<HTMLDivElement>();
const [stripePromise, setStripePromise] =
useState<Promise<Stripe | null> | null>(null);
const {products, placementId, paywallId} = usePaywall({
placementKey,
});
const [isOpenCardModal, setIsOpenCardModal] = useState(false);
const {
paymentIntentId,
clientSecret,
returnUrl: checkoutUrl,
paymentType,
publicKey,
isLoading: isLoadingPayment,
error,
} = useMakePayment({
productId: activeProduct?._id || '',
placementId,
paywallId,
returnPaidUrl: returnUrl,
});
const [isLoadingExpressCheckout, setIsLoadingExpressCheckout] =
useState(true);
const isLoading = useMemo(() => {
return isLoadingPayment || isLoadingExpressCheckout;
}, [isLoadingPayment, isLoadingExpressCheckout]);
if (checkoutUrl?.length) {
window.location.href = checkoutUrl;
}
useEffect(() => {
(async () => {
if (!products?.length || !publicKey) return;
setStripePromise(loadStripe(publicKey));
const isActiveProduct = products.find(
(product) => product._id === activeProduct?._id,
);
if (!activeProduct || !isActiveProduct) {
navigate(routes.client.trialChoice());
}
})();
}, [activeProduct, navigate, products, publicKey]);
const resizeHandler = () => {
setHeight?.(ref?.current?.clientHeight || 32);
};
if (error?.length) {
setTimeout(resizeHandler, 300);
return (
<div ref={ref as LegacyRef<HTMLDivElement>} className={styles['payment-modal']}>
<Title variant="h3" className={styles.title}>
Something went wrong
</Title>
</div>
);
}
return (
<div ref={ref as LegacyRef<HTMLDivElement>}
className={cn(styles.paymentModalContainer, isLoading && styles.paymentModalContainerLoading)}>
{isLoading && <div className={cn(styles.paymentModalLoader)}>
<Loader/>
</div>
}
<div className={styles.paymentModalPrice}>Total due today:
${getFormattedPrice(activeProduct.trialPrice)}</div>
{!isLoadingPayment &&
<>
{!isLoading &&
<div className={styles.paymentCreditCard} onClick={() => setIsOpenCardModal(true)}>
<CreditCardIcon/>
<div>Credit / Debit Card</div>
</div>
}
<Elements stripe={stripePromise} options={{clientSecret}}>
<ExpressCheckoutStripe
clientSecret={clientSecret!}
returnUrl={returnUrl}
paymentMethodOrderList={['google_pay', 'apple_pay', 'link']}
onChangeLoading={(isLoading) => {
setIsLoadingExpressCheckout(isLoading);
setTimeout(resizeHandler, 300);
}
}
/>
</Elements>
<PaymentCardModal
isOpen={isOpenCardModal}
setIsOpen={setIsOpenCardModal}
clientSecret={clientSecret}
stripePromise={stripePromise}
paymentType={paymentType}
paymentIntentId={paymentIntentId}
returnUrl={returnUrl}
/>
{!isLoading &&
<>
<div className={styles.infoContainer}>
<SecurityPayments/>
<p className={styles.address}>1123 Rimer Dr Moraga, California 94556</p>
</div>
</>
}
</>
}
</div>
);
}

View File

@ -0,0 +1,60 @@
.paymentModalContainer {
display: flex;
flex-direction: column;
position: relative;
margin: -12px -20px;
padding: 12px 20px;
gap: 6px;
transition: height 1s ease-out;
.address {
color: gray;
font-size: 10px;
margin-bottom: 16px;
text-transform: uppercase;
}
.infoContainer > * {
padding-top: 16px;
}
.paymentCreditCard {
background: #066fde;
color: #fff !important;
gap: 6px;
display: flex;
font-size: 14px;
line-height: 18px;
align-items: center;
font-weight: 400;
min-height: 48px;
border-radius: 5px;
justify-content: center;
}
&Loading {
background: rgba(215, 213, 213, .5);
}
.paymentModalLoader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 25px;
color: #2f2e37;
position: absolute;
width: 100%;
margin-left: -20px;
}
.paymentModalPrice {
color: #066fde;
font-size: 16px;
font-weight: 700;
line-height: 25px;
text-align: center;
margin-bottom: 12px;
}
}

View File

@ -12,11 +12,13 @@ import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
export type TConfirmType = "payment" | "setup";
interface ICheckoutFormProps { interface ICheckoutFormProps {
children?: JSX.Element | null; children?: JSX.Element | null;
subscriptionReceiptId?: string; subscriptionReceiptId?: string;
returnUrl?: string; returnUrl?: string;
confirmType?: "payment" | "setup"; confirmType?: TConfirmType;
isHide?: boolean; isHide?: boolean;
} }

View File

@ -21,6 +21,7 @@ interface IExpressCheckoutStripeProps {
availableMethods: AvailablePaymentMethods | undefined availableMethods: AvailablePaymentMethods | undefined
) => void; ) => void;
onChangeLoading?: (isLoading: boolean) => void; onChangeLoading?: (isLoading: boolean) => void;
paymentMethodOrderList?: string[];
} }
function ExpressCheckoutStripe({ function ExpressCheckoutStripe({
@ -29,6 +30,7 @@ function ExpressCheckoutStripe({
isHide = false, isHide = false,
onAvailable, onAvailable,
onChangeLoading, onChangeLoading,
paymentMethodOrderList
}: IExpressCheckoutStripeProps) { }: IExpressCheckoutStripeProps) {
const stripe = useStripe(); const stripe = useStripe();
const elements = useElements(); const elements = useElements();
@ -104,7 +106,7 @@ function ExpressCheckoutStripe({
maxColumns: 1, maxColumns: 1,
overflow: "never", overflow: "never",
}, },
paymentMethodOrder: ["apple_pay", "google_pay", "amazon_pay", "link"], paymentMethodOrder: paymentMethodOrderList || ["apple_pay", "google_pay", "amazon_pay", "link"],
wallets: { wallets: {
googlePay: "always", googlePay: "always",
applePay: "always", applePay: "always",

View File

@ -184,13 +184,17 @@
} }
.payment-screen__widget { .payment-screen__widget {
position: fixed;
background: #fff; background: #fff;
bottom: 0; bottom: 0;
box-shadow: 0 -2px 16px rgba(18, 22, 32, .1); box-shadow: 0 -2px 16px rgba(18, 22, 32, .1);
max-width: 428px;
width: 100%; width: 100%;
padding: 40px; padding: 12px 20px;
position: relative; text-align: center;
text-align: -webkit-center;
transition: .5s height;
max-width: 560px;
margin-left: -66px;
} }
.payment-screen__widget_success { .payment-screen__widget_success {
@ -200,9 +204,6 @@
.payment-screen__success { .payment-screen__success {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: absolute;
left: 0;
top: 0;
background: #fff; background: #fff;
z-index: 99; z-index: 99;
display: flex; display: flex;
@ -226,3 +227,21 @@
text-align: center; text-align: center;
color: #121620; color: #121620;
} }
.payment-screen__widget_modal_container {
height: 100%;
}
@media screen and (max-width: 560px) {
.payment-screen__widget {
width: 100%;
padding: 12px 20px;
text-align: center;
text-align: -webkit-center;
transition: height 1s linear;
max-width: 560px;
margin-left: 0;
left: 0;
}
}

View File

@ -1,20 +1,17 @@
import React from "react"; import React, { useState } from 'react';
import { useSelector } from "react-redux"; import { useSelector } from 'react-redux';
import "./payment-screen.css"; import './payment-screen.css';
import useSteps, { Step } from "@/hooks/palmistry/use-steps"; import useSteps, { Step } from '@/hooks/palmistry/use-steps';
import useTimer from "@/hooks/palmistry/use-timer"; import useTimer from '@/hooks/palmistry/use-timer';
import HeaderLogo from "@/components/palmistry/header-logo/header-logo"; import HeaderLogo from '@/components/palmistry/header-logo/header-logo';
import PaymentModal from "@/components/PaymentModal"; import { selectors } from '@/store';
import { selectors } from "@/store"; import { EPlacementKeys } from '@/api/resources/Paywall';
import { EPlacementKeys } from "@/api/resources/Paywall"; import { useSearchParams } from 'react-router-dom';
import { useSearchParams } from "react-router-dom"; import PaymentModalNew from '@/components/PaymentModalNew';
import { getFormattedPrice } from '@/utils/price.utils';
const getFormattedPrice = (price: number) => {
return (price / 100).toFixed(2);
};
export default function PaymentScreen() { export default function PaymentScreen() {
const time = useTimer(); const time = useTimer();
@ -24,7 +21,7 @@ export default function PaymentScreen() {
searchParams.get("redirect_status") === "succeeded" searchParams.get("redirect_status") === "succeeded"
? "subscribed" ? "subscribed"
: "lead"; : "lead";
// const subscriptionStatus = useSelector(selectors.selectStatus); const [height, setHeight] = useState(subscriptionStatus === "subscribed" ? 246 : 146);
const steps = useSteps(); const steps = useSteps();
@ -52,7 +49,7 @@ export default function PaymentScreen() {
return ( return (
<div className="payment-screen"> <div className="payment-screen">
<div className="payment-screen__header"> <div className="payment-screen__header">
<HeaderLogo /> <HeaderLogo/>
</div> </div>
<div className="payment-screen__content"> <div className="payment-screen__content">
@ -246,6 +243,8 @@ export default function PaymentScreen() {
<style>{`.palmistry-payment-modal { max-height: calc(100dvh - 40px) }`}</style> <style>{`.palmistry-payment-modal { max-height: calc(100dvh - 40px) }`}</style>
{activeProductFromStore && ( {activeProductFromStore && (
<div className="payment-screen__widget_modal_container"
style={{minHeight: `${height}px`}}>
<div <div
className={`payment-screen__widget${ className={`payment-screen__widget${
subscriptionStatus === "subscribed" subscriptionStatus === "subscribed"
@ -254,7 +253,9 @@ export default function PaymentScreen() {
}`} }`}
> >
{subscriptionStatus !== "subscribed" && ( {subscriptionStatus !== "subscribed" && (
<PaymentModal <PaymentModalNew
setHeight={setHeight}
activeProduct={activeProductFromStore}
returnUrl={window.location.href} returnUrl={window.location.href}
placementKey={EPlacementKeys["aura.placement.palmistry.main"]} placementKey={EPlacementKeys["aura.placement.palmistry.main"]}
/> />
@ -281,6 +282,7 @@ export default function PaymentScreen() {
</div> </div>
)} )}
</div> </div>
</div>
)} )}
</div> </div>
); );

View File

@ -0,0 +1,3 @@
export const getFormattedPrice = (price: number) => {
return (price / 100).toFixed(2);
};