Merge branch 'develop' into 'main'

develop

See merge request witapp/aura-webapp!505
This commit is contained in:
Daniil Chemerkin 2024-12-30 23:43:24 +00:00
commit aaedc42362
12 changed files with 200 additions and 77 deletions

View File

@ -35,11 +35,6 @@
); );
</script> </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 charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
@ -184,6 +179,21 @@
</head> </head>
<body> <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 --> <!-- Klaviyo Metric -->
<script type="module"> <script type="module">
const klaviyoKeys = { const klaviyoKeys = {

View File

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

View File

@ -1,5 +1,5 @@
import routes from "@/routes"; import routes from "@/routes";
import { getAuthHeaders } from "../utils"; import { getAuthHeaders, getBaseHeaders } from "../utils";
interface Payload { interface Payload {
token: string; token: string;
@ -50,3 +50,17 @@ export const createRequestPost = ({ token, productId, placementId, paywallId, pa
}); });
return new Request(url, { method: "POST", headers: getAuthHeaders(token), body }); 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 OftenAsk from "./components/OftenAsk";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import WithPartnerInformation from "./components/WithPartnerInformation"; import WithPartnerInformation from "./components/WithPartnerInformation";
import Modal from "@/components/Modal";
import { trialPaymentPointsList } from "@/data/pointsLists"; import { trialPaymentPointsList } from "@/data/pointsLists";
import { trialPaymentReviews } from "@/data/reviews"; import { trialPaymentReviews } from "@/data/reviews";
import TrialPaymentHeader from "./components/Header"; import TrialPaymentHeader from "./components/Header";
import Header from "../../components/Header"; import Header from "../../components/Header";
import BackgroundTopBlob from "../../ui/BackgroundTopBlob"; import BackgroundTopBlob from "../../ui/BackgroundTopBlob";
import { useDynamicSize } from "@/hooks/useDynamicSize"; 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 { usePaywall } from "@/hooks/paywall/usePaywall";
import PaymentModal from "@/components/PaymentModal";
import metricService, { import metricService, {
EGoals, EGoals,
EMetrics, EMetrics,
} from "@/services/metric/metricService"; } from "@/services/metric/metricService";
import { useTranslations } from "@/hooks/translations"; import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales"; 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() { function TrialPaymentPage() {
const { translate } = useTranslations(ELocalesPlacement.V1); const { translate } = useTranslations(ELocalesPlacement.V1);
@ -49,19 +51,38 @@ function TrialPaymentPage() {
} = useSelector(selectors.selectQuestionnaire); } = useSelector(selectors.selectQuestionnaire);
const partnerZodiacSign = getZodiacSignByDate(partnerBirthdate); const partnerZodiacSign = getZodiacSignByDate(partnerBirthdate);
const { products } = usePaywall({ const { products } = usePaywall({
placementKey: EPlacementKeys["aura.placement.redesign.main"], placementKey,
localesPlacement: ELocalesPlacement.V1, localesPlacement: ELocalesPlacement.V1,
}); });
const activeProductFromStore = useSelector(selectors.selectActiveProduct); const activeProduct = useSelector(selectors.selectActiveProduct);
const [activeProduct, setActiveProduct] = useState<IPaywallProduct | null>(
activeProductFromStore
);
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
const [singleOrWithPartner, setSingleOrWithPartner] = useState< const [singleOrWithPartner, setSingleOrWithPartner] = useState<
"single" | "partner" "single" | "partner"
>("single"); >("single");
const { subPlan } = useParams(); 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(() => { useEffect(() => {
metricService.reachGoal(EGoals.AURA_TRIAL_PAYMENT_PAGE_VISIT, [ metricService.reachGoal(EGoals.AURA_TRIAL_PAYMENT_PAGE_VISIT, [
EMetrics.KLAVIYO, EMetrics.KLAVIYO,
@ -79,11 +100,10 @@ function TrialPaymentPage() {
) === subPlan ) === subPlan
); );
if (targetProduct) { if (targetProduct) {
setActiveProduct(targetProduct); dispatch(actions.payment.update({ activeProduct: targetProduct }));
dispatch(actions.payment.update({ activeProduct }));
} }
} }
}, [dispatch, subPlan, products, activeProduct]); }, [dispatch, subPlan, products]);
useEffect(() => { useEffect(() => {
if (!products.length) return; if (!products.length) return;
@ -112,13 +132,12 @@ function TrialPaymentPage() {
} }
const handleDiscount = () => { const handleDiscount = () => {
setIsOpenPaymentModal(false);
navigate(routes.client.additionalDiscountV1()); navigate(routes.client.additionalDiscountV1());
}; };
const openStripeModal = () => { const openPaymentModal = () => {
metricService.reachGoal(EGoals.AURA_PAYMENT_METHODS_OPENED); metricService.reachGoal(EGoals.AURA_PAYMENT_METHODS_OPENED);
setIsOpenPaymentModal(true); showCreditCardForm()
}; };
return ( return (
@ -129,16 +148,11 @@ function TrialPaymentPage() {
backgroundColor: gender === "male" ? "#C1E5FF" : "#F7EBFF", backgroundColor: gender === "male" ? "#C1E5FF" : "#F7EBFF",
}} }}
> >
<Modal {isLoading &&
containerClassName={styles.modal} <div className={styles["loader-container"]}>
open={isOpenPaymentModal} <Loader color={LoaderColor.White} />
onClose={handleDiscount} </div>
type="hidden" }
>
<PaymentModal
placementKey={EPlacementKeys["aura.placement.redesign.main"]}
/>
</Modal>
<div className={styles["background-top-blob-container"]}> <div className={styles["background-top-blob-container"]}>
<BackgroundTopBlob <BackgroundTopBlob
width={pageWidth} width={pageWidth}
@ -148,7 +162,7 @@ function TrialPaymentPage() {
</div> </div>
<Header className={styles.header} /> <Header className={styles.header} />
<TrialPaymentHeader <TrialPaymentHeader
buttonClick={openStripeModal} buttonClick={openPaymentModal}
buttonText={translate("/trial-payment.button") as string} buttonText={translate("/trial-payment.button") as string}
/> />
{singleOrWithPartner === "partner" && ( {singleOrWithPartner === "partner" && (
@ -178,13 +192,13 @@ function TrialPaymentPage() {
<PaymentTable <PaymentTable
gender={gender} gender={gender}
product={activeProduct} product={activeProduct}
buttonClick={openStripeModal} buttonClick={openPaymentModal}
placementKey={EPlacementKeys["aura.placement.redesign.main"]} placementKey={placementKey}
/> />
<YourReading <YourReading
gender={gender} gender={gender}
zodiacSign={zodiacSign} zodiacSign={zodiacSign}
buttonClick={openStripeModal} buttonClick={openPaymentModal}
singleOrWithPartner={singleOrWithPartner} singleOrWithPartner={singleOrWithPartner}
callToActionText={translate("/trial-payment.to_read_full") as string} callToActionText={translate("/trial-payment.to_read_full") as string}
buttonText={translate("/trial-payment.button") as string} buttonText={translate("/trial-payment.button") as string}
@ -201,8 +215,8 @@ function TrialPaymentPage() {
<PaymentTable <PaymentTable
gender={gender} gender={gender}
product={activeProduct} product={activeProduct}
buttonClick={openStripeModal} buttonClick={openPaymentModal}
placementKey={EPlacementKeys["aura.placement.redesign.main"]} placementKey={placementKey}
/> />
</section> </section>
); );

View File

@ -46,3 +46,16 @@
padding: 0 14px; padding: 0 14px;
width: 100%; 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 styles from "./styles.module.css";
import PaymentDiscountTable from "./PaymentDiscountTable"; import PaymentDiscountTable from "./PaymentDiscountTable";
import Modal from "@/components/Modal"; import { useEffect } from "react";
import { useState } from "react";
import { selectors } from "@/store"; import { selectors } from "@/store";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { EPlacementKeys } from "@/api/resources/Paywall"; import { EPlacementKeys } from "@/api/resources/Paywall";
import PaymentModal from "@/components/PaymentModal";
import { useTranslations } from "@/hooks/translations"; import { useTranslations } from "@/hooks/translations";
import { addCurrency, ELocalesPlacement } from "@/locales"; import { addCurrency, ELocalesPlacement } from "@/locales";
import DiscountLayout from "../../layouts/Discount/DiscountLayout"; import DiscountLayout from "../../layouts/Discount/DiscountLayout";
import QuestionnaireGreenButton from "../../ui/GreenButton"; 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() { function TrialPaymentWithDiscount() {
const navigate = useNavigate()
const { translate } = useTranslations(ELocalesPlacement.V1); const { translate } = useTranslations(ELocalesPlacement.V1);
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
const activeProduct = useSelector(selectors.selectActiveProduct); const activeProduct = useSelector(selectors.selectActiveProduct);
const currency = useSelector(selectors.selectCurrency); const currency = useSelector(selectors.selectCurrency);
const handleClose = () => { if (!activeProduct) {
setIsOpenPaymentModal(false); 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 ( return (
<DiscountLayout title={translate("/trial-payment-with-discount.title")}> <DiscountLayout title={translate("/trial-payment-with-discount.title")}>
<Modal {isLoading &&
containerClassName={styles.modal} <div className={styles["loader-container"]}>
open={isOpenPaymentModal} <Loader color={LoaderColor.White} />
onClose={handleClose} </div>
type="hidden" }
>
<PaymentModal
placementKey={EPlacementKeys["aura.placement.secret.discount"]}
/>
</Modal>
<PaymentDiscountTable /> <PaymentDiscountTable />
<div className={styles['button-wrapper']}> <div className={styles['button-wrapper']}>
<QuestionnaireGreenButton <QuestionnaireGreenButton
className={styles.button} className={styles.button}
onClick={() => setIsOpenPaymentModal(true)} onClick={openPaymentModal}
> >
{translate("/trial-payment-with-discount.button", { {translate("/trial-payment-with-discount.button", {
trialDuration: activeProduct?.trialDuration, trialDuration: activeProduct?.trialDuration,

View File

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

View File

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

View File

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

View File

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