This commit is contained in:
Daniil Chemerkin 2024-12-30 23:43:24 +00:00
parent 59462be48b
commit 4f1e05da22
12 changed files with 200 additions and 77 deletions

View File

@ -35,11 +35,6 @@
);
</script>
<!-- NMI CollectJS -->
<script src="https://hms.transactiongateway.com/token/Collect.js" data-tokenization-key="MMJJDe-ESZqfT-7P4f8z-mwD8N4"
data-variant="inline"></script>
<!-- <script src="https://hms.transactiongateway.com/js/v1/Gateway.js"></script> -->
<!-- NMI CollectJS -->
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
@ -184,6 +179,21 @@
</head>
<body>
<!-- Load NMI Collect.js -->
<script type="module">
import { createApi } from "/src/api/api.ts"
const api = createApi();
api.getPaymentConfig(null).then((paymentConfig) => {
const nmiPublicKey = paymentConfig?.data?.nmi?.publicKey;
const scriptElement = document.createElement("script");
scriptElement.src = "https://hms.transactiongateway.com/token/Collect.js";
scriptElement.setAttribute("data-tokenization-key", nmiPublicKey);
scriptElement.setAttribute("data-variant", "inline");
document.head.appendChild(scriptElement);
})
</script>
<!-- Load NMI Collect.js -->
<!-- Klaviyo Metric -->
<script type="module">
const klaviyoKeys = {

View File

@ -91,6 +91,7 @@ const api = {
getPaywallByPlacementKey: createMethod<Paywall.PayloadGet, Paywall.ResponseGet>(Paywall.createRequestGet),
// Payment
makePayment: createMethod<Payment.PayloadPost, Payment.ResponsePost>(Payment.createRequestPost),
getPaymentConfig: createMethod<null, Payment.IPaymentConfigResponse>(Payment.getConfigRequest),
// User videos
getUserVideos: createMethod<UserVideos.PayloadGet, UserVideos.ResponseGet>(UserVideos.createRequest),
// User PDF

View File

@ -1,5 +1,5 @@
import routes from "@/routes";
import { getAuthHeaders } from "../utils";
import { getAuthHeaders, getBaseHeaders } from "../utils";
interface Payload {
token: string;
@ -50,3 +50,17 @@ export const createRequestPost = ({ token, productId, placementId, paywallId, pa
});
return new Request(url, { method: "POST", headers: getAuthHeaders(token), body });
};
export interface IPaymentConfigResponse {
status: "success" | string,
data: {
nmi: {
publicKey: string
}
}
}
export const getConfigRequest = (): Request => {
const url = new URL(routes.server.getPaymentConfig());
return new Request(url, { method: "GET", headers: getBaseHeaders() });
};

View File

@ -14,22 +14,24 @@ import PointsList from "./components/PointsList";
import OftenAsk from "./components/OftenAsk";
import { useEffect, useState } from "react";
import WithPartnerInformation from "./components/WithPartnerInformation";
import Modal from "@/components/Modal";
import { trialPaymentPointsList } from "@/data/pointsLists";
import { trialPaymentReviews } from "@/data/reviews";
import TrialPaymentHeader from "./components/Header";
import Header from "../../components/Header";
import BackgroundTopBlob from "../../ui/BackgroundTopBlob";
import { useDynamicSize } from "@/hooks/useDynamicSize";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import { EPlacementKeys } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import PaymentModal from "@/components/PaymentModal";
import metricService, {
EGoals,
EMetrics,
} from "@/services/metric/metricService";
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import { usePayment } from "@/hooks/payment/nmi/usePayment";
import Loader, { LoaderColor } from "@/components/Loader";
const placementKey = EPlacementKeys["aura.placement.redesign.main"]
function TrialPaymentPage() {
const { translate } = useTranslations(ELocalesPlacement.V1);
@ -49,19 +51,38 @@ function TrialPaymentPage() {
} = useSelector(selectors.selectQuestionnaire);
const partnerZodiacSign = getZodiacSignByDate(partnerBirthdate);
const { products } = usePaywall({
placementKey: EPlacementKeys["aura.placement.redesign.main"],
placementKey,
localesPlacement: ELocalesPlacement.V1,
});
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
const [activeProduct, setActiveProduct] = useState<IPaywallProduct | null>(
activeProductFromStore
);
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
const activeProduct = useSelector(selectors.selectActiveProduct);
const [singleOrWithPartner, setSingleOrWithPartner] = useState<
"single" | "partner"
>("single");
const { subPlan } = useParams();
const { isLoading, isModalClosed, error, isPaymentSuccess, showCreditCardForm } = usePayment({
placementKey,
activeProduct: activeProduct!
});
useEffect(() => {
if (isPaymentSuccess) {
return navigate(routes.client.paymentSuccess())
}
}, [isPaymentSuccess])
useEffect(() => {
if (isModalClosed) {
return handleDiscount()
}
}, [isModalClosed])
useEffect(() => {
if (error?.length && error !== "Product not found") {
return navigate(routes.client.paymentFail())
}
}, [error])
useEffect(() => {
metricService.reachGoal(EGoals.AURA_TRIAL_PAYMENT_PAGE_VISIT, [
EMetrics.KLAVIYO,
@ -79,11 +100,10 @@ function TrialPaymentPage() {
) === subPlan
);
if (targetProduct) {
setActiveProduct(targetProduct);
dispatch(actions.payment.update({ activeProduct }));
dispatch(actions.payment.update({ activeProduct: targetProduct }));
}
}
}, [dispatch, subPlan, products, activeProduct]);
}, [dispatch, subPlan, products]);
useEffect(() => {
if (!products.length) return;
@ -112,13 +132,12 @@ function TrialPaymentPage() {
}
const handleDiscount = () => {
setIsOpenPaymentModal(false);
navigate(routes.client.additionalDiscountV1());
};
const openStripeModal = () => {
const openPaymentModal = () => {
metricService.reachGoal(EGoals.AURA_PAYMENT_METHODS_OPENED);
setIsOpenPaymentModal(true);
showCreditCardForm()
};
return (
@ -129,16 +148,11 @@ function TrialPaymentPage() {
backgroundColor: gender === "male" ? "#C1E5FF" : "#F7EBFF",
}}
>
<Modal
containerClassName={styles.modal}
open={isOpenPaymentModal}
onClose={handleDiscount}
type="hidden"
>
<PaymentModal
placementKey={EPlacementKeys["aura.placement.redesign.main"]}
/>
</Modal>
{isLoading &&
<div className={styles["loader-container"]}>
<Loader color={LoaderColor.White} />
</div>
}
<div className={styles["background-top-blob-container"]}>
<BackgroundTopBlob
width={pageWidth}
@ -148,7 +162,7 @@ function TrialPaymentPage() {
</div>
<Header className={styles.header} />
<TrialPaymentHeader
buttonClick={openStripeModal}
buttonClick={openPaymentModal}
buttonText={translate("/trial-payment.button") as string}
/>
{singleOrWithPartner === "partner" && (
@ -178,13 +192,13 @@ function TrialPaymentPage() {
<PaymentTable
gender={gender}
product={activeProduct}
buttonClick={openStripeModal}
placementKey={EPlacementKeys["aura.placement.redesign.main"]}
buttonClick={openPaymentModal}
placementKey={placementKey}
/>
<YourReading
gender={gender}
zodiacSign={zodiacSign}
buttonClick={openStripeModal}
buttonClick={openPaymentModal}
singleOrWithPartner={singleOrWithPartner}
callToActionText={translate("/trial-payment.to_read_full") as string}
buttonText={translate("/trial-payment.button") as string}
@ -201,8 +215,8 @@ function TrialPaymentPage() {
<PaymentTable
gender={gender}
product={activeProduct}
buttonClick={openStripeModal}
placementKey={EPlacementKeys["aura.placement.redesign.main"]}
buttonClick={openPaymentModal}
placementKey={placementKey}
/>
</section>
);

View File

@ -45,4 +45,17 @@
top: 32px;
padding: 0 14px;
width: 100%;
}
.loader-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100dvh;
z-index: 9999;
background-color: #000000bb;
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -1,44 +1,65 @@
import styles from "./styles.module.css";
import PaymentDiscountTable from "./PaymentDiscountTable";
import Modal from "@/components/Modal";
import { useState } from "react";
import { useEffect } from "react";
import { selectors } from "@/store";
import { useSelector } from "react-redux";
import { EPlacementKeys } from "@/api/resources/Paywall";
import PaymentModal from "@/components/PaymentModal";
import { useTranslations } from "@/hooks/translations";
import { addCurrency, ELocalesPlacement } from "@/locales";
import DiscountLayout from "../../layouts/Discount/DiscountLayout";
import QuestionnaireGreenButton from "../../ui/GreenButton";
import { usePayment } from "@/hooks/payment/nmi/usePayment";
import { Navigate, useNavigate } from "react-router-dom";
import routes from "@/routes";
import Loader, { LoaderColor } from "@/components/Loader";
function TrialPaymentWithDiscount() {
const navigate = useNavigate()
const { translate } = useTranslations(ELocalesPlacement.V1);
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
const activeProduct = useSelector(selectors.selectActiveProduct);
const currency = useSelector(selectors.selectCurrency);
const handleClose = () => {
setIsOpenPaymentModal(false);
if (!activeProduct) {
return <Navigate to={routes.client.additionalDiscountV1()} />
}
const { isLoading, error, isPaymentSuccess, showCreditCardForm } = usePayment({
placementKey: EPlacementKeys["aura.placement.secret.discount"],
activeProduct: activeProduct!
});
useEffect(() => {
if (isPaymentSuccess) {
return navigate(routes.client.paymentSuccess())
}
}, [isPaymentSuccess])
useEffect(() => {
if (error?.length && error !== "Product not found") {
return navigate(routes.client.paymentFail())
}
}, [error])
const openPaymentModal = () => {
showCreditCardForm()
};
return (
<DiscountLayout title={translate("/trial-payment-with-discount.title")}>
<Modal
containerClassName={styles.modal}
open={isOpenPaymentModal}
onClose={handleClose}
type="hidden"
>
<PaymentModal
placementKey={EPlacementKeys["aura.placement.secret.discount"]}
/>
</Modal>
{isLoading &&
<div className={styles["loader-container"]}>
<Loader color={LoaderColor.White} />
</div>
}
<PaymentDiscountTable />
<div className={styles['button-wrapper']}>
<QuestionnaireGreenButton
className={styles.button}
onClick={() => setIsOpenPaymentModal(true)}
onClick={openPaymentModal}
>
{translate("/trial-payment-with-discount.button", {
trialDuration: activeProduct?.trialDuration,

View File

@ -32,8 +32,21 @@
transform: translate(-50%, 0);
}
.loader-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100dvh;
z-index: 9999;
background-color: #000000bb;
display: flex;
justify-content: center;
align-items: center;
}
@media (max-width: 390px) {
.button {
font-size: 26px;
}
}
}

View File

@ -0,0 +1,30 @@
import { useEffect } from "react";
const useElementRemovalObserver = (
targetId: string,
onRemove: (removedNode: HTMLElement) => void,
container: HTMLElement | null = document.body
): void => {
useEffect(() => {
if (!container) {
console.warn("Контейнер для наблюдения не найден");
return;
}
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.removedNodes.forEach((node) => {
if (node instanceof HTMLElement && node.id === targetId) {
onRemove(node);
}
});
});
});
observer.observe(container, { childList: true, subtree: true });
return () => observer.disconnect();
}, [targetId, onRemove, container]);
};
export default useElementRemovalObserver;

View File

@ -1,6 +1,7 @@
import { useApi } from "@/api";
import { ResponsePost } from "@/api/resources/Payment";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import useElementRemovalObserver from "@/hooks/DOM/useElementRemovalObserver";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { selectors } from "@/store";
import { useEffect, useMemo, useState } from "react"
@ -19,6 +20,15 @@ export const usePayment = ({ placementKey, activeProduct }: IUsePaymentProps) =>
const [error, setError] = useState<string | null>(null);
const [isPaymentSuccess, setIsPaymentSuccess] = useState(false);
const formPrice = String((activeProduct?.trialPrice || 99) / 100);
const [isOpenModal, setIsOpenModal] = useState(false);
const [isModalClosed, setIsModalClosed] = useState(false);
const updatePaymentModalState = () => {
setIsOpenModal(false);
setIsModalClosed(true);
}
useElementRemovalObserver("CollectJSIframe", updatePaymentModalState)
const isLoading = useMemo(() => {
return isSubmitting
@ -38,9 +48,9 @@ export const usePayment = ({ placementKey, activeProduct }: IUsePaymentProps) =>
}, [products]);
useEffect(() => {
if (!activeProduct) return;
window.CollectJS.configure({
variant: 'lightbox',
// styleSniffer: true,
callback: (token: any) => {
finishSubmit(token);
},
@ -64,7 +74,7 @@ export const usePayment = ({ placementKey, activeProduct }: IUsePaymentProps) =>
// country: "US",
// currency: "USD",
});
}, [placementId, paywallId]);
}, [placementId, paywallId, activeProduct]);
const finishSubmit = async (response: any) => {
try {
@ -79,6 +89,9 @@ export const usePayment = ({ placementKey, activeProduct }: IUsePaymentProps) =>
});
setPaymentResponse(res);
setIsPaymentSuccess(res.status === "paid");
if (res.status !== "paid") {
setError("message" in res ? res.message : "Something went wrong")
}
} catch (error: any) {
console.error('Payment error:', error);
setError(error?.message);
@ -87,10 +100,11 @@ export const usePayment = ({ placementKey, activeProduct }: IUsePaymentProps) =>
}
};
const showCreditCardForm = () => {
window.CollectJS.startPaymentRequest({
const showCreditCardForm = async () => {
await window.CollectJS.startPaymentRequest({
amount: formPrice,
});
setIsOpenModal(true)
};
return useMemo(() => ({
@ -98,12 +112,16 @@ export const usePayment = ({ placementKey, activeProduct }: IUsePaymentProps) =>
paymentResponse,
error,
isPaymentSuccess,
isOpenModal,
isModalClosed,
showCreditCardForm
}), [
isLoading,
paymentResponse,
error,
isPaymentSuccess,
isOpenModal,
isModalClosed,
showCreditCardForm
])
}

View File

@ -22,7 +22,7 @@ import App from "./components/App";
import metricService from "./services/metric/metricService";
import "core-js/actual";
import { pdfjs } from "react-pdf";
import MetaPixel from "./utils/FBMetaPixel";
import HeadData from "./utils/Helmet";
pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.js`;
@ -90,7 +90,7 @@ const init = async () => {
return (
<React.Fragment>
<MetaPixel />
<HeadData />
<I18nextProvider i18n={i18nextInstance}>
<Provider store={store}>
<BrowserRouter>

View File

@ -383,6 +383,7 @@ const routes = {
// Payment
makePayment: () => [dApiHost, dApiPrefix, "payment", "checkout"].join("/"),
getPaymentConfig: () => [dApiHost, dApiPrefix, "payment", "config"].join("/"),
// User videos
getUserVideos: () => [dApiHost, "users", "videos", "combined"].join("/"),

View File

@ -20,7 +20,7 @@ const isRouteInclude = (url: string, routes: string[]) => {
return false;
};
const MetaPixel = () => {
const HeadData = () => {
const locale = getDefaultLocaleByLanguage(language);
const isPalmistry = isRouteInclude(window.location.pathname, routesPalmistry);
const isChats = isRouteInclude(window.location.pathname, routesChats);
@ -166,13 +166,7 @@ fbq('track', 'PageView');`;
{isPalmistry && <script>{FBScriptPalmistry2}</script>}
{isPalmistry && <script>{FBScriptPalmistry3}</script>}
{isPalmistry && locale === "es" && (
<>
<script>{FBScriptPalmistryES1}</script>
<script>{FBScriptPalmistryES2}</script>
</>
)}
{isPalmistry && locale === "es" && (
<script>{FBScriptPalmistryES2}</script>
<script>{FBScriptPalmistryES1}</script>
)}
{isPalmistry && locale === "es" && (
<script>{FBScriptPalmistryES2}</script>
@ -181,13 +175,7 @@ fbq('track', 'PageView');`;
<script>{FBScriptPalmistryPT1}</script>
)}
{isPalmistry && locale === "en" && (
<>
<script>{FBScriptPalmistryEN1}</script>
<script>{FBScriptPalmistryEN2}</script>
</>
)}
{isPalmistry && locale === "en" && (
<script>{FBScriptPalmistryEN2}</script>
<script>{FBScriptPalmistryEN1}</script>
)}
{isPalmistry && locale === "en" && (
<script>{FBScriptPalmistryEN2}</script>
@ -205,4 +193,4 @@ fbq('track', 'PageView');`;
);
};
export default MetaPixel;
export default HeadData;