develop
This commit is contained in:
parent
59462be48b
commit
4f1e05da22
20
index.html
20
index.html
@ -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 = {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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() });
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/hooks/DOM/useElementRemovalObserver.ts
Normal file
30
src/hooks/DOM/useElementRemovalObserver.ts
Normal 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;
|
||||
@ -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
|
||||
])
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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("/"),
|
||||
|
||||
@ -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;
|
||||
Loading…
Reference in New Issue
Block a user