Merge branch 'develop' into 'main'

develop

See merge request witapp/aura-webapp!532
This commit is contained in:
Daniil Chemerkin 2025-01-19 23:50:56 +00:00
commit 4b3626de00
76 changed files with 1853 additions and 1675 deletions

View File

@ -683,6 +683,12 @@
"description": "By continuing you agree that if you don't cancel prior to the end of the <trialDuration>-days trial, you will automatically be charged <price> for the introductory period of 14 days thereafter the standard rate of <price> every 14 days until you cancel in settings. Learn more about cancellation and refund policy in Subscription terms."
},
"/trial-choice": {
"text_variant_1": "AURA is the only accurate app with reliable astrological predictions, verified by professionals and guaranteed to provide accurate astrological forecasts. <br><br> AURA has already helped millions of people find happiness and discover the whole truth about their relationships. <br><br> An astrological forecast that will completely change your life is almost ready! Before we provide it to you, we would like to offer you the opportunity to choose the amount you consider reasonable to try AURA for <trialDuration> days and which you think is fair for the changes that will happen to you: <br><br> - You will discover all the most intimate secrets that the stars have prepared for you and solve relationship issues within just one month; <br> - You will once and for all put the finishing touches on unresolved issues and forget about problems that have been haunting you for years (if not decades); <br> - You will save hundreds of dollars on fake and unprofessional astrological predictions and fortune tellers; <br> - You will receive not only a personal astrological forecast but also personalized daily horoscopes, learn who and how is draining your energy, and get other personalized readings. <br><br> <span>",
"text_variant_1_span": "A <trialDuration>-day trial period costs us $5, but please choose the amount that suits you best:",
"text_variant_2": "AURA is the only app you can trust for accurate astrological insights, crafted and verified by seasoned professionals, ensuring you receive predictions that are both reliable and transformative. <br><br> AURA has already transformed the lives of millions, bringing clarity, joy, and a deeper understanding of their relationships. <br><br> Your life-changing astrological forecast is almost ready! But before we reveal the secrets that will change your life, we want to give you the freedom to choose how much you feel is fair to try AURA for <trialDuration> days. This is your chance to decide what the transformation is worth to you: <br><br> - Uncover the deepest, most intimate secrets the stars have in store for you, and watch your relationship issues resolve in just one month; <br> - Finally, put an end to those lingering issues that have been troubling you for years, maybe even decades; <br> - Save hundreds of dollars by avoiding unreliable astrologers and fake fortune tellers; <br> - Receive not just a personal astrological forecast but also personalized daily horoscopes, learn who's draining your energy, and get exclusive, tailored readings. <br><br><span>",
"text_variant_2_span": "While a <trialDuration>-day trial costs us $5, we want you to choose the amount you believe is right for you:",
"text_variant_3": "Discover AURA—the only app that delivers truly accurate astrological forecasts, with predictions you can trust, all verified by top industry professionals. Your path to clarity and happiness starts here. <br><br> Millions have already found happiness and uncovered the truth about their relationships with AURA. Now, its your turn. <br><br> Your life-changing astrological forecast is almost ready! Before we share this powerful insight with you, were giving you the chance to set your own price to experience AURA for <trialDuration> days. You decide what feels right for the life-changing revelations youll receive: <br><br> - Reveal the deepest secrets the universe has in store for you and resolve your relationship dilemmas within a month; <br> - Finally close the chapter on long-standing issues that have plagued you for years, perhaps even decades; <br> - Avoid wasting hundreds of dollars on untrustworthy, fake astrologers; <br> - Gain access to your personal astrological forecast, receive daily personalized horoscopes, learn whos been draining your energy, and benefit from other insightful readings. <br><br><span>",
"text_variant_3_span": "Our <trialDuration>-day trial typically costs us $5, but you get to choose the amount that feels right for you:",
"button": "Choose an amount that you think is reasonable."
},
"skip_trial": "Skip Trial",

View File

@ -0,0 +1 @@
{"data":{"subscription_popup":true,"locale":"en","alerts":{},"version":"5.0","apple_music_api":{"jwt":"eyJhbGciOiJFUzI1NiIsImtpZCI6IlRSUDZXTUtERFYifQ.eyJpc3MiOiJLVEFMQTg4WkNSIiwiaWF0IjoxNzM2OTUyODM3LCJleHAiOjE3MzcwODI0Mzd9.0XjbuMOZ_7EcejBb5WEQxWH0tAkd1VqbbFJnYDDaKgpoLBd2fM7nYYHEOugKnmfWaJJt-OguJpmKI6opG1ilPg"},"chargebee":{"site":"kefirapp-test","publishableKey":"test_VtWSamZEfP175nqGZhkD0uvoouHieElv"},"first_open_subscription_popup":true,"runs_before_subscription_popup":0,"stripe_public_key":"pk_live_51Ndqf4IlX4lgwUxrdyEW5BKH30OLEYemyVj3XFqi3RNx3K149o0jNQEswuIutBXNQ4CeqJuODh6OMT9I3r1fq3VT00ncnJjWov","smartlook_manage":false,"appirater_alerts":[],"active_iaps":[{"bundle_id":"auraweb.yearlymembership","active":true,"subscription_type":"yearly"},{"bundle_id":"auraweb.weeklymembership","active":true,"subscription_type":"trial"},{"bundle_id":"auraweb.weekmembership","active":true,"subscription_type":"trial"},{"bundle_id":"auraweb.monthlymembership","active":true,"subscription_type":"monthly"}]}}

View File

@ -0,0 +1 @@
{"data":{"variant":"a","groups":[]}}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 4.5 MiB

View File

@ -2,15 +2,12 @@ import { createMethod } from './utils'
import {
User,
Auras,
Element,
Elements,
AuthTokens,
Apps,
Assets,
AssetCategories,
DailyForecasts,
SubscriptionItems,
SubscriptionCheckout,
SubscriptionStatus,
AICompatCategories,
AICompats,
@ -18,9 +15,6 @@ import {
UserCallbacks,
Translations,
Zodiacs,
GoogleAuth,
SubscriptionPlans,
AppleAuth,
AIRequestsV2,
Assistants,
OpenAI,
@ -41,12 +35,10 @@ import {
} from './resources'
const api = {
auth: createMethod<AuthTokens.Payload, AuthTokens.Response>(AuthTokens.createRequest),
appleAuth: createMethod<AppleAuth.Payload, AppleAuth.Response>(AppleAuth.createRequest),
googleAuth: createMethod<GoogleAuth.Payload, GoogleAuth.Response>(GoogleAuth.createRequest),
// auth: createMethod<AuthTokens.Payload, AuthTokens.Response>(AuthTokens.createRequest),
getRealToken: createMethod<AuthTokens.PayloadGetRealToken, AuthTokens.ResponseGetRealToken>(AuthTokens.createGetRealTokenRequest),
getAppConfig: createMethod<Apps.Payload, Apps.Response>(Apps.createRequest),
getElement: createMethod<Element.Payload, Element.Response>(Element.createRequest),
// getElement: createMethod<Element.Payload, Element.Response>(Element.createRequest),
getElements: createMethod<Elements.Payload, Elements.Response>(Elements.createRequest),
getUser: createMethod<User.GetPayload, User.Response>(User.createGetRequest),
updateUser: createMethod<User.PatchPayload, User.Response>(User.createPatchRequest),
@ -54,9 +46,8 @@ const api = {
getAssetCategories: createMethod<AssetCategories.Payload, AssetCategories.Response>(AssetCategories.createRequest),
getDailyForecasts: createMethod<DailyForecasts.Payload, DailyForecasts.Response>(DailyForecasts.createRequest),
getAuras: createMethod<Auras.Payload, Auras.Response>(Auras.createRequest),
getSubscriptionItems: createMethod<SubscriptionItems.Payload, SubscriptionItems.Response>(SubscriptionItems.createRequest),
getSubscriptionPlans: createMethod<SubscriptionPlans.Payload, SubscriptionPlans.Response>(SubscriptionPlans.createRequest),
getSubscriptionCheckout: createMethod<SubscriptionCheckout.Payload, SubscriptionCheckout.Response>(SubscriptionCheckout.createRequest),
// getSubscriptionPlans: createMethod<SubscriptionPlans.Payload, SubscriptionPlans.Response>(SubscriptionPlans.createRequest),
// getSubscriptionCheckout: createMethod<SubscriptionCheckout.Payload, SubscriptionCheckout.Response>(SubscriptionCheckout.createRequest),
getSubscriptionStatus: createMethod<SubscriptionStatus.Payload, SubscriptionStatus.Response>(SubscriptionStatus.createRequest),
// new get subscription status
getSubscriptionStatusNew: createMethod<SubscriptionStatus.Payload, SubscriptionStatus.ResponseNew>(SubscriptionStatus.createRequestNew),
@ -101,6 +92,7 @@ const api = {
// Session
createSession: createMethod<Session.PayloadCreate, Session.ResponseCreate>(Session.createRequest),
updateSession: createMethod<Session.PayloadUpdate, Session.ResponseUpdate>(Session.updateRequest),
getLocaleTranslations: createMethod<Session.PayloadGetLocale, Session.ResponseGetLocale>(Session.getLocaleRequest),
// Chats
getChatsCategories: createMethod<null, ChatsCategories.ResponseGet>(ChatsCategories.getRequest),
getChatMessages: createMethod<ChatMessages.Payload, ChatMessages.ResponseGet>(ChatMessages.getRequest),

View File

@ -1,13 +0,0 @@
import routes from "@/routes";
export interface Payload {
origin: string;
}
export type Response = unknown;
export const createRequest = ({ origin }: Payload): Request => {
const url = new URL(routes.server.appleAuth(origin));
return new Request(url, { method: "POST" });
};

View File

@ -1,7 +1,7 @@
import routes from "@/routes";
import { AuthToken } from "../types";
import { User } from "./User";
import { getAuthHeaders, getBaseHeaders } from "../utils";
import { getAuthHeaders } from "../utils";
export interface PayloadRegisterByEmail {
email: string;
@ -13,7 +13,7 @@ export interface PayloadAuthWithJWT {
jwt: string;
}
export type Payload = PayloadRegisterByEmail | PayloadAuthWithJWT;
// export type Payload = PayloadRegisterByEmail | PayloadAuthWithJWT;
export interface Response {
auth: {
@ -36,11 +36,11 @@ export interface JwtPayload {
iss: string;
}
export const createRequest = (payload: Payload): Request => {
const url = new URL(routes.server.token());
const body = JSON.stringify({ auth: { ...payload } });
return new Request(url, { method: "POST", headers: getBaseHeaders(), body });
};
// export const createRequest = (payload: Payload): Request => {
// const url = new URL(routes.server.token());
// const body = JSON.stringify({ auth: { ...payload } });
// return new Request(url, { method: "POST", headers: getBaseHeaders(), body });
// };
export interface PayloadGetRealToken {
token: string;

View File

@ -1,31 +0,0 @@
import routes from '@/routes'
import { getBaseHeaders } from '../utils'
export interface Payload {
locale: string
type: string
}
export interface Response {
data: {
variant: string
element: Element
}
}
export interface Element {
type: string
href: string
title: string
url_slug: string
body: string
}
export const createRequest = ({ locale, type }: Payload): Request => {
const url = new URL(routes.server.element(type))
const query = new URLSearchParams({ locale })
url.search = query.toString()
return new Request(url, { method: 'GET', headers: getBaseHeaders() })
}

View File

@ -1,14 +0,0 @@
export interface Payload {
requestUrl: string;
}
export interface Response {
access_token: string;
}
export const createRequest = ({ requestUrl }: Payload): Request => {
const url = new URL(requestUrl);
return new Request(url, {
method: "GET",
});
};

View File

@ -17,7 +17,9 @@ export enum EPlacementKeys {
"aura.placement.secret.discount" = "aura.placement.secret.discount",
"aura.placement.palmistry.main" = "aura.placement.palmistry.main",
"aura.placement.palmistry.redesign" = "aura.placement.palmistry.redesign",
"aura.placement.chat" = "aura.placement.chat"
"aura.placement.chat" = "aura.placement.chat",
"aura.placement.email.palmistry" = "aura.placement.email.palmistry",
"aura.placement.email.palmistry.discount" = "aura.placement.email.palmistry.discount"
}
export interface ResponseGetSuccess {

View File

@ -2,6 +2,7 @@ import routes from "@/routes";
import { getBaseHeaders } from "../utils";
import { IUTM } from "@/store/utm";
import { ICreateAuthorizeUser } from "./User";
import { ELocalesPlacement } from "@/locales";
export interface PayloadCreate {
feature: string, // Type: string
@ -78,6 +79,15 @@ export interface ResponseUpdate {
message: "Session updated" | string
}
export interface ResponseGetLocale {
male: Record<string, string>
female: Record<string, string>
fallback: {
male: Record<string, string>,
female: Record<string, string>
}
}
export const createRequest = (data: PayloadCreate) => {
const url = new URL(routes.server.createSession());
const body = JSON.stringify(data);
@ -97,3 +107,18 @@ export const updateRequest = ({ data, sessionId }: PayloadUpdate) => {
headers: getBaseHeaders()
});
};
export interface PayloadGetLocale {
funnel: ELocalesPlacement,
locale: string
}
export const getLocaleRequest = (data: PayloadGetLocale) => {
const url = new URL(routes.server.getLocale());
const body = JSON.stringify(data);
return new Request(url, {
method: "POST",
body,
headers: getBaseHeaders()
});
};

View File

@ -1,43 +0,0 @@
import routes from "@/routes";
export interface Payload {
locale: string;
}
export interface Response {
sub_plans: ISubscriptionPlan[];
}
export interface ISubscriptionPlan {
id: string;
name: string;
desc: string;
provider: "stripe" | "paypal";
interval: "week" | "month" | "year";
price_cents: number;
trial: ITrial | null;
}
export interface ITrial {
is_paid: boolean;
is_free: boolean;
days: number;
price_cents: number;
}
export interface AssetMetadata {
size: number;
width: number;
height: number;
filename: string;
mime_type: string;
}
export const createRequest = ({ locale }: Payload): Request => {
const url = new URL(routes.server.subscriptionPlans());
const query = new URLSearchParams({ locale });
url.search = query.toString();
return new Request(url, { method: "GET" });
};

View File

@ -1,35 +0,0 @@
import routes from "@/routes"
import { AuthPayload } from "../types"
import { getAuthHeaders } from "../utils"
export interface Payload extends AuthPayload {
embed?: boolean
locale: string
itemPriceId: string
}
export interface Response {
hosted_page: string
}
export interface HostedPage {
id: string
url: string
embed: boolean
type: string
object: string
state: string
resource_version: number
created_at: number
updated_at: number
expires_at: number
}
export const createRequest = ({ locale, token, itemPriceId, embed = false }: Payload): Request => {
const url = new URL(routes.server.subscriptionCheckout())
const query = new URLSearchParams({ locale, item_price_id: itemPriceId, embed: embed.toString() })
url.search = query.toString()
return new Request(url, { method: 'GET', headers: getAuthHeaders(token) })
}

View File

@ -1,40 +0,0 @@
import routes from "@/routes"
import { AuthPayload } from "../types"
import { getAuthHeaders } from "../utils"
export interface Payload extends AuthPayload {
locale: string
}
export interface Response {
item_prices: ItemPrice[]
}
export interface ItemPrice {
currency_code: string
external_name: string
free_quantity: number
id: string
is_taxable: boolean
item_id: string
item_type: string
name: string
object: string
period: number
period_unit: string
price: number
pricing_model: string
resource_version: number
status: string
created_at: number
updated_at: number
}
export const createRequest = ({ locale, token }: Payload): Request => {
const url = new URL(routes.server.subscriptionItems())
const query = new URLSearchParams({ locale })
url.search = query.toString()
return new Request(url, { method: 'GET', headers: getAuthHeaders(token) })
}

View File

@ -1,104 +0,0 @@
import routes from "@/routes";
import { AuthPayload } from "../types";
import { getAuthHeaders } from "../utils";
export interface GetPayload extends AuthPayload {
id: string;
}
export interface AppleReceiptPayload extends AuthPayload {
receiptData: string;
autorenewable?: boolean;
sandbox?: boolean;
}
export interface StripeReceiptPayload extends AuthPayload {
way: "stripe";
subscription_receipt: {
sub_plan_id: string;
};
}
export type Payload =
| AppleReceiptPayload
| StripeReceiptPayload
export interface Response {
subscription_receipt: SubscriptionReceipt;
}
export interface SubscriptionReceipt {
id: string;
user_id: number;
status: number;
expires_at: null | string;
requested_at: string;
created_at: string;
data: {
input: {
subscription_items: [
{
item_price_id: string;
}
];
payment_intent: {
gw_token: string;
gateway_account_id: string;
};
};
client_secret: string;
app_bundle_id: string;
autorenewable: boolean;
error: string;
stripe_status?: string;
checkout_url?: string;
checkout_session?: unknown;
};
}
function createRequest({
token,
receiptData,
autorenewable,
sandbox,
}: AppleReceiptPayload): Request;
function createRequest({ token }: StripeReceiptPayload): Request;
function createRequest(payload: Payload): Request;
function createRequest(payload: Payload): Request {
const url = new URL(routes.server.subscriptionReceipts());
const data = getDataPayload(payload);
const body = JSON.stringify(data);
return new Request(url, {
method: "POST",
headers: getAuthHeaders(payload.token),
body,
});
}
function getDataPayload(payload: Payload) {
if ("receiptData" in payload) {
return {
way: "apple",
subscription_receipt: {
receipt_data: payload.receiptData,
autorenewable: payload.autorenewable,
sandbox: payload.sandbox,
},
};
}
if ("way" in payload && payload.way === "stripe") {
return {
way: "stripe",
subscription_receipt: {
sub_plan_id: payload.subscription_receipt.sub_plan_id,
},
};
}
}
function createGetRequest({ id, token }: GetPayload): Request {
const url = new URL(routes.server.subscriptionReceipt(id));
return new Request(url, { method: "GET", headers: getAuthHeaders(token) });
}
export { createRequest, createGetRequest };

View File

@ -4,11 +4,8 @@ export * as Apps from "./Apps";
export * as User from "./User";
export * as DailyForecasts from "./UserDailyForecasts";
export * as Auras from "./Auras";
export * as Element from "./Element";
export * as Elements from "./Elements";
export * as AuthTokens from "./AuthTokens";
export * as SubscriptionItems from "./UserSubscriptionItemPrices";
export * as SubscriptionCheckout from "./UserSubscriptionCheckout";
export * as SubscriptionStatus from "./UserSubscriptionStatus";
export * as AICompatCategories from "./AICompatCategories";
export * as AICompats from "./AICompats";
@ -16,9 +13,6 @@ export * as AIRequests from "./AIRequests";
export * as UserCallbacks from "./UserCallbacks";
export * as Translations from "./Translations";
export * as Zodiacs from "./Zodiacs";
export * as GoogleAuth from "./GoogleAuth";
export * as SubscriptionPlans from "./SubscriptionPlans";
export * as AppleAuth from "./AppleAuth";
export * as AIRequestsV2 from "./AIRequestsV2";
export * as Assistants from "./Assistants";
export * as OpenAI from "./OpenAI";

View File

@ -34,10 +34,8 @@ import BirthdayPage from "../BirthdayPage";
import BirthtimePage from "../BirthtimePage";
import CreateProfilePage from "../CreateProfilePage";
import EmailEnterPage from "../EmailEnterPage";
import SubscriptionPage from "../SubscriptionPage";
import PaymentPage from "../PaymentPage";
import WallpaperPage from "../WallpaperPage";
import StaticPage from "../StaticPage";
import NotFoundPage from "../NotFoundPage";
import Header from "../Header";
import Navbar from "../Navbar";
@ -45,7 +43,6 @@ import Footer from "../Footer";
import "./styles.css";
import DidYouKnowPage from "../DidYouKnowPage";
import FreePeriodInfoPage from "../FreePeriodInfoPage";
import AttentionPage from "../AttentionPage";
import FeedbackPage from "../FeedbackPage";
import CompatibilityPage from "../Compatibility";
import BreathPage from "../BreathPage";
@ -147,13 +144,11 @@ if (isProduction) {
function App(): JSX.Element {
const location = useLocation();
const [isSpecialOfferOpen, setIsSpecialOfferOpen] = useState<boolean>(false);
const [leoApng, setLeoApng] = useState<Error | APNG>(Error);
// const [
// padLockApng,
// setPadLockApng,
// ] = useState<Error | APNG>(Error);
const navigate = useNavigate();
const api = useApi();
const dispatch = useDispatch();
const { token, user, signUp, logout } = useAuth();
@ -228,11 +223,6 @@ function App(): JSX.Element {
});
}, []);
const closeSpecialOfferAttention = () => {
setIsSpecialOfferOpen(false);
navigate(routes.client.auth());
};
const assetsData = useCallback(async () => {
const { assets } = await api.getAssets({
category: String("au"),
@ -345,7 +335,7 @@ function App(): JSX.Element {
element={<MikeV1Routes />}
/>
<Route
element={<Layout setIsSpecialOfferOpen={setIsSpecialOfferOpen} />}
element={<Layout />}
>
<Route path={routes.client.loadingPage()} element={<LoadingPage />} />
{/* Email - Pay - Email */}
@ -852,15 +842,6 @@ function App(): JSX.Element {
path={routes.client.freePeriodInfo()}
element={<FreePeriodInfoPage />}
/>
<Route
path={routes.client.attention()}
element={
<AttentionPage
isOpenModal={isSpecialOfferOpen}
onCloseSpecialOffer={closeSpecialOfferAttention}
/>
}
/>
<Route path={routes.client.feedback()} element={<FeedbackPage />} />
<Route
path={routes.client.birthtime()}
@ -882,20 +863,20 @@ function App(): JSX.Element {
path={routes.client.authResult()}
element={<AuthResultPage />}
/>
<Route path={routes.client.static()} element={<StaticPage />} />
{/* <Route path={routes.client.static()} element={<StaticPage />} /> */}
<Route
path={routes.client.priceList()}
element={<PriceListPage />}
/>
</Route>
<Route element={<AuthorizedUserOutlet />}>
{/* <Route element={<AuthorizedUserOutlet />}>
<Route
path={routes.client.subscription()}
element={<SubscriptionPage />}
>
<Route path=":subPlan" element={<SubscriptionPage />} />
</Route>
</Route>
</Route> */}
<Route element={<PrivateOutlet />}>
<Route element={<AuthorizedUserOutlet />}>
<Route
@ -975,11 +956,7 @@ function App(): JSX.Element {
);
}
interface LayoutProps {
setIsSpecialOfferOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
function Layout({ setIsSpecialOfferOpen }: LayoutProps): JSX.Element {
function Layout(): JSX.Element {
const location = useLocation();
const navigate = useNavigate();
const dispatch = useDispatch();
@ -988,7 +965,6 @@ function Layout({ setIsSpecialOfferOpen }: LayoutProps): JSX.Element {
const showHeader = hasNoHeader(location.pathname);
const isRouteFullDataModal = hasFullDataModal(location.pathname);
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
const changeIsSpecialOfferOpen = () => setIsSpecialOfferOpen(true);
const homeConfig = useSelector(selectors.selectHome);
const showNavbarFooter = homeConfig.isShowNavbar;
const mainRef = useRef<HTMLDivElement>(null);
@ -1081,7 +1057,6 @@ function Layout({ setIsSpecialOfferOpen }: LayoutProps): JSX.Element {
{showHeader ? (
<Header
openMenu={() => setIsMenuOpen(true)}
clickCross={changeIsSpecialOfferOpen}
/>
) : null}
{isRouteFullDataModal && (

View File

@ -1,40 +0,0 @@
import { useNavigate } from "react-router-dom";
import { useTranslations } from "@/hooks/translations";
import Title from "../Title";
import routes from "@/routes";
import styles from "./styles.module.css";
import SpecialWelcomeOffer from "../SpecialWelcomeOffer";
import MainButton from "../MainButton";
interface AttentionPageProps {
isOpenModal: boolean;
onCloseSpecialOffer?: () => void;
}
function AttentionPage({
isOpenModal,
onCloseSpecialOffer,
}: AttentionPageProps): JSX.Element {
const { translate } = useTranslations();
const navigate = useNavigate();
const handleNext = () => navigate(routes.client.priceList());
return (
<section className={`${styles.page} page`}>
<SpecialWelcomeOffer open={isOpenModal} onClose={onCloseSpecialOffer} />
<img className={styles.icon} src="/stop-icon.webp" alt="stop" />
<Title variant="h2">{translate("aura.attention.title")}</Title>
<p className={styles.text}>{translate("aura.warming_up.body")}</p>
<div className={styles["buttons-container"]}>
<MainButton onClick={handleNext}>
{translate("aura.warmin_good.button")}
</MainButton>
<MainButton onClick={handleNext}>
{translate("aura.warmin_bad.button")}
</MainButton>
</div>
</section>
);
}
export default AttentionPage;

View File

@ -1,33 +0,0 @@
.page {
position: relative;
flex: auto;
height: calc(100vh - 50px);
max-height: -webkit-fill-available;
justify-content: center;
gap: 16px;
}
.icon {
width: 96px;
height: 96px;
}
.text {
text-align: center;
line-height: 1.2;
font-weight: 700;
}
.buttons-container {
width: 100%;
margin: 64px auto 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 13px;
}
.button {
white-space: pre;
cursor: pointer;
}

View File

@ -1,17 +0,0 @@
import MainButton from "@/components/MainButton";
import styles from "./styles.module.css";
interface IAppleAuthButtonProps {
onClick: () => void;
}
function AppleAuthButton({ onClick }: IAppleAuthButtonProps): JSX.Element {
return (
<MainButton className={styles.button} onClick={onClick}>
<img src="apple-auth-icon.webp" alt="Apple" />
{"Sign in with Apple"}
</MainButton>
);
}
export default AppleAuthButton;

View File

@ -1,11 +0,0 @@
.button {
width: 100%;
background-color: transparent;
color: #000;
font-size: 19px;
font-weight: 600;
border: solid #000 2px;
flex-direction: row;
justify-content: flex-start;
gap: 20px;
}

View File

@ -1,17 +0,0 @@
import MainButton from "@/components/MainButton";
import styles from "./styles.module.css";
interface IGoogleAuthButtonProps {
onClick: () => void;
}
function AppleAuthButton({ onClick }: IGoogleAuthButtonProps): JSX.Element {
return (
<MainButton className={styles.button} onClick={onClick}>
<img src="google-auth-icon.svg" alt="Google" />
{"Sign in with Google"}
</MainButton>
);
}
export default AppleAuthButton;

View File

@ -1,11 +0,0 @@
.button {
width: 100%;
background-color: transparent;
color: #000;
font-size: 19px;
font-weight: 600;
border: solid #4285f4 2px;
flex-direction: row;
justify-content: flex-start;
gap: 20px;
}

View File

@ -1,96 +0,0 @@
import Policy from "../Policy";
import { useTranslations } from "@/hooks/translations";
import styles from "./styles.module.css";
import AppleAuthButton from "./AppleAuthButton";
import routes from "@/routes";
import Title from "../Title";
import { APNG } from "apng-js";
import Player from "apng-js/types/library/player";
import { useEffect, useRef } from "react";
import GoogleAuthButton from "./GoogleAuthButton";
let apngPlayer: Player | null = null;
interface AuthPageProps {
padLockApng: Error | APNG;
}
function AuthPage({ padLockApng }: AuthPageProps): JSX.Element {
const { translate } = useTranslations();
const padLockCanvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
let padLockTimeOut: NodeJS.Timeout;
async function getApngPlayer() {
const context = padLockCanvasRef.current?.getContext("2d");
if (context && !(padLockApng instanceof Error)) {
context.canvas.height = padLockApng.height;
context.canvas.width = padLockApng.width;
const _apngPlayer = await padLockApng.getPlayer(context);
apngPlayer = _apngPlayer;
if (apngPlayer) {
apngPlayer.play();
padLockTimeOut = setTimeout(() => {
if (apngPlayer) {
apngPlayer.pause();
}
}, 900);
}
}
}
getApngPlayer();
return () => {
clearTimeout(padLockTimeOut);
};
}, [padLockApng]);
const handleAppleAuth = async () => {
window.location.href = routes.server.appleAuth(
encodeURI(`${window.location.origin}/auth/result/`)
);
};
const handleGoogleAuth = async () => {
window.location.href = routes.server.googleAuth(
encodeURI(`${window.location.origin}/auth/result/`)
);
};
return (
<section className={`${styles.page} page`}>
<Title variant="h2" className={styles.title}>
Sign in to save your energy analysis, horoscope, and predictions.
</Title>
<canvas className={styles["pad-lock"]} ref={padLockCanvasRef} />
<p className={styles.disclaimer}>{translate("we_dont_share")}</p>
<div className={styles["buttons-container"]}>
<GoogleAuthButton onClick={handleGoogleAuth} />
<AppleAuthButton onClick={handleAppleAuth} />
</div>
<Policy className={styles.policy} sizing="medium">
{translate("_continue_agree", {
eulaLink: (
<a
href="https://aura.wit.life/terms"
target="_blank"
rel="noopener noreferrer"
>
{translate("eula")}
</a>
),
privacyLink: (
<a
href="https://aura.wit.life/privacy"
target="_blank"
rel="noopener noreferrer"
>
{translate("privacy_policy")}
</a>
),
})}
</Policy>
</section>
);
}
export default AuthPage;

View File

@ -1,41 +0,0 @@
.page {
position: relative;
/* height: calc(100vh - 103px);
max-height: -webkit-fill-available; */
flex: auto;
justify-content: flex-start;
display: flex;
grid-template-rows: 1fr 96px;
justify-items: center;
}
.disclaimer {
font-size: 19px;
font-weight: 500;
text-align: center;
margin-top: 24px;
line-height: 150%;
}
.title {
font-weight: 700;
margin: 32px 0 0;
}
.pad-lock {
width: 76px;
margin-top: 48px;
}
.buttons-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
max-width: 320px;
margin-top: 42px;
}
.policy {
margin-top: 32px;
}

View File

@ -6,7 +6,7 @@ function AdviceFromAstrologer() {
return (
<div className={styles.container}>
<TextWithEmoji text="1:1 Advice from your personal astrologer" emoji="sparkling-heart.svg" />
<img className={styles.messages} src={images("messages.png")} alt="messages" />
<img className={styles.messages} src={images("messages.svg")} alt="messages" />
</div>
)
}

View File

@ -10,6 +10,8 @@
margin-top: 26px;
box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.46),
inset 6px 6px 82px 0px rgba(0, 0, 0, 0.25);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
.circularText {
@ -17,20 +19,34 @@
width: 100%;
height: 100%;
animation: rotate 20s linear infinite;
-webkit-transform: translateZ(0);
-webkit-perspective: 1000;
-webkit-backface-visibility: hidden;
svg {
width: 100%;
height: 100%;
-webkit-transform: translateZ(0);
}
text {
fill: white;
font-size: 8px;
font-weight: 600;
-webkit-font-smoothing: antialiased;
&:nth-of-type(2) {
transform: rotate(180deg);
transform-origin: center;
-webkit-transform: rotate(180deg) translateZ(0);
-moz-transform: rotate(180deg) translateZ(0);
-ms-transform: rotate(180deg) translateZ(0);
-o-transform: rotate(180deg) translateZ(0);
transform: rotate(180deg) translateZ(0);
-webkit-transform-origin: center center;
-moz-transform-origin: center center;
-ms-transform-origin: center center;
-o-transform-origin: center center;
transform-origin: center center;
}
}
}
@ -45,10 +61,22 @@
@keyframes rotate {
from {
transform: rotate(0deg);
-webkit-transform: rotate(0deg) translateZ(0);
transform: rotate(0deg) translateZ(0);
}
to {
transform: rotate(360deg);
-webkit-transform: rotate(360deg) translateZ(0);
transform: rotate(360deg) translateZ(0);
}
}
@-webkit-keyframes rotate {
from {
-webkit-transform: rotate(0deg) translateZ(0);
}
to {
-webkit-transform: rotate(360deg) translateZ(0);
}
}

View File

@ -6,7 +6,7 @@ function FindingPartner() {
return (
<div className={styles.container}>
<TextWithEmoji text="Finding the most compatible partner" emoji="revolving-hearts.svg" />
<img className={styles.smartphone} src={images("smartphone.png")} alt="smartphone" />
<img className={styles.smartphone} src={images("smartphone.svg")} alt="smartphone" />
</div>
)
}

View File

@ -4,7 +4,7 @@ import styles from "./styles.module.scss";
function Payments() {
return (
<div className={styles.container}>
<img src={images("payments.svg")} alt="payments" />
<img src={images("payments.png")} alt="payments" />
</div>
)
}

View File

@ -20,6 +20,7 @@ import { useNavigate } from "react-router-dom";
import routes from "@/routes";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall";
import BlurComponent from "@/components/BlurComponent";
const features = [
{
@ -171,9 +172,13 @@ function MarketingLanding() {
</div>
<GuaranteedSecurityPayments />
<Payments />
<Button className={styles.buttonContinue} onClick={handleContinue}>
Continue
</Button>
<div className={styles.buttonContainer}>
<BlurComponent isActiveBlur={true}>
<Button className={styles.buttonContinue} onClick={handleContinue}>
Continue
</Button>
</BlurComponent>
</div>
</div>
)
}

View File

@ -6,7 +6,7 @@
width: 100%;
max-width: 460px;
margin: 0 auto;
padding-bottom: 64px;
padding-bottom: 140px;
overflow-x: hidden;
}
@ -67,11 +67,6 @@
padding-left: 34px;
}
.buttonContinue {
max-width: 300px;
margin-top: 30px;
}
.relative-container {
position: relative;
width: 100%;
@ -152,4 +147,23 @@
.backgroundElement14 {
bottom: -50px;
right: -30px;
}
.buttonContainer {
width: 100%;
display: flex;
justify-content: center;
position: fixed;
bottom: 0;
pointer-events: none;
z-index: 9999;
.buttonContinue {
position: relative;
z-index: 1000;
max-width: 300px;
margin-top: 48px;
margin-bottom: 64px;
pointer-events: all;
}
}

View File

@ -8,15 +8,21 @@ import { actions, selectors } from "@/store";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall";
import Modal from "@/components/Modal";
import PaymentModal from "@/components/PaymentModal";
import PaymentForm from "@/components/Payment/nmi/PaymentForm";
import routes from "@/routes";
import { useNavigate } from "react-router-dom";
import BlurComponent from "@/components/BlurComponent";
const placementKey = EPlacementKeys["aura.placement.email.marketing"];
function SpecialOffer() {
const dispatch = useDispatch();
const navigate = useNavigate();
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
const activeProduct = useSelector(selectors.selectActiveProduct);
const { products, getText } = usePaywall({
placementKey: EPlacementKeys["aura.placement.email.marketing"],
placementKey,
});
const trialPrice = ((products[0]?.trialPrice || 0) / 100).toFixed(2) || 0;
@ -27,7 +33,7 @@ function SpecialOffer() {
dispatch(actions.payment.update({ activeProduct: products[0] }));
}, [dispatch, products]);
const openStripeModal = () => {
const openPaymentModal = () => {
setIsOpenPaymentModal(true);
};
@ -35,6 +41,14 @@ function SpecialOffer() {
setIsOpenPaymentModal(false);
};
const onPaymentError = () => {
return navigate(routes.client.paymentFail())
}
const onPaymentSuccess = () => {
return navigate(routes.client.paymentSuccess())
}
return (
<>
{products[0] && (
@ -44,8 +58,10 @@ function SpecialOffer() {
onClose={handleCloseModal}
type="hidden"
>
<PaymentModal
placementKey={EPlacementKeys["aura.placement.email.marketing"]}
<PaymentForm
placementKey={placementKey}
onPaymentError={onPaymentError}
onPaymentSuccess={onPaymentSuccess}
/>
</Modal>
)}
@ -67,12 +83,16 @@ function SpecialOffer() {
trialDuration={trialDuration}
saveText={getText("text.save") as string}
/>
<Button className={styles.button} onClick={openStripeModal}>
Continue
</Button>
<p className={styles.contentPolicy}>
By continuing you agree that if you don't cancel prior to the end of the {trialDuration}-days trial, you will automatically be charged ${price} every 2 weeks until you cancel in settings. Learn more about cancellation and refund policy in Subscription terms
</p>
<div className={styles.buttonContainer}>
<BlurComponent isActiveBlur={true}>
<Button className={styles.button} onClick={openPaymentModal}>
Continue
</Button>
</BlurComponent>
</div>
</div>
</div>
</>

View File

@ -25,7 +25,7 @@
border-radius: 30px 30px 0 0;
min-height: calc(100dvh - 39px - 26px * 1.25 - 29px);
margin-top: 29px;
padding: 53px 18px 46px;
padding: 53px 18px 160px;
color: #000;
display: flex;
flex-direction: column;
@ -51,7 +51,7 @@
line-height: 125%;
font-weight: 300;
margin-bottom: 0;
margin-top: 61px;
margin-top: 36px;
text-align: center;
}
@ -59,4 +59,23 @@
margin-top: 59px;
max-width: 307px;
}
}
.buttonContainer {
width: 100%;
display: flex;
justify-content: center;
position: fixed;
bottom: 0;
pointer-events: none;
z-index: 1000;
.button {
position: relative;
z-index: 1000;
max-width: 300px;
margin-top: 24px;
margin-bottom: 64px;
pointer-events: all;
}
}

View File

@ -1,43 +1,43 @@
import { useTranslations } from '@/hooks/translations';
import { addCurrency, ELocalesPlacement } from '@/locales';
import styles from "./styles.module.scss";
import { LegacyRef, useEffect, useRef } from 'react';
import { LegacyRef, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { selectors } from '@/store';
import { EPlacementKeys, IPaywallProduct } from '@/api/resources/Paywall';
import Loader from '@/components/Loader';
import cn from "classnames";
import { getFormattedPrice } from '@/utils/price.utils';
import SecurityPayments from '@/components/pages/TrialPayment/components/SecurityPayments';
import { usePayment } from '@/hooks/payment/nmi/usePayment';
import Title from '@/components/Title';
import metricService, { EGoals, EMetrics } from '@/services/metric/metricService';
import { useNavigate } from 'react-router-dom';
import routes from '@/routes';
import CreditCardIcon from '@/components/PaymentModalNew/PaymentCardModal/CreditCardIcon';
import Modal from '@/components/Modal';
import NMIPaymentForm from '@/components/Payment/nmi/PaymentForm';
interface IPaymentFormProps {
placementKey: EPlacementKeys;
activeProduct: IPaywallProduct;
}
function PaymentForm({
activeProduct,
placementKey,
}: IPaymentFormProps) {
const navigate = useNavigate();
const { translate } = useTranslations(ELocalesPlacement.PalmistryV1);
const ref = useRef<HTMLDivElement>();
const currency = useSelector(selectors.selectCurrency);
const [isPaymentSuccess, setIsPaymentSuccess] = useState(false);
const [isPaymentError, setIsPaymentError] = useState(false);
const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
const { isLoading, error, isPaymentSuccess, showCreditCardForm } = usePayment({
placementKey,
activeProduct
});
const onPaymentError = () => {
setIsPaymentError(true);
}
useEffect(() => {
if (!isPaymentSuccess) return;
const onPaymentSuccess = () => {
setIsPaymentSuccess(true);
metricService.reachGoal(EGoals.PAYMENT_SUCCESS);
metricService.reachGoal(EGoals.PAYMENT_SUCCESS_PALMISTRY, [
EMetrics.YANDEX,
@ -48,13 +48,12 @@ function PaymentForm({
value: ((activeProduct.trialPrice || 100) / 100).toFixed(2),
});
}
const redirectTimeout = setTimeout(() => {
setTimeout(() => {
navigate(routes.client.skipTrial());
}, 1500);
return () => clearTimeout(redirectTimeout);
}, [isPaymentSuccess])
}
if (error?.length) {
if (isPaymentError) {
return (
<div
ref={ref as LegacyRef<HTMLDivElement>}
@ -97,15 +96,13 @@ function PaymentForm({
<div
ref={ref as LegacyRef<HTMLDivElement>}
className={cn(
styles.paymentModalContainer,
isLoading && styles.paymentModalContainerLoading
styles.paymentModalContainer
)}
>
{isLoading && (
<div className={cn(styles.paymentModalLoader)}>
<Loader />
</div>
)}
<Modal containerClassName={styles["modal-content"]} open={isPaymentModalOpen}>
<NMIPaymentForm onPaymentError={onPaymentError} onPaymentSuccess={onPaymentSuccess} placementKey={EPlacementKeys['aura.placement.palmistry.redesign']} />
</Modal>
<div className={styles.paymentModalPrice}>
{translate(
@ -119,29 +116,25 @@ function PaymentForm({
ELocalesPlacement.PalmistryV1
)}
</div>
{!isLoading && (
<>
<div
className={styles.paymentCreditCard}
onClick={showCreditCardForm}
>
<CreditCardIcon />
<div>Credit / Debit Card</div>
</div>
{/* <GooglePayButton />
<div
className={styles.paymentCreditCard}
onClick={() => setIsPaymentModalOpen(true)}
>
<CreditCardIcon />
<div>Credit / Debit Card</div>
</div>
{/* <GooglePayButton />
<ApplePayButton /> */}
<div className={styles.infoContainer}>
<SecurityPayments />
<p className={styles.address}>
{translate(
"payment_modal.address",
undefined,
ELocalesPlacement.V1
)}
</p>
</div>
</>
)}
<div className={styles.infoContainer}>
<SecurityPayments />
<p className={styles.address}>
{translate(
"payment_modal.address",
undefined,
ELocalesPlacement.V1
)}
</p>
</div>
</div>
);
}

View File

@ -86,4 +86,9 @@
text-align: center;
color: #121620;
}
}
.modal-content {
overflow-x: hidden;
}

View File

@ -3,7 +3,6 @@ import { HTMLAttributes } from 'react';
import { useSelector } from 'react-redux';
import styles from "./styles.module.scss";
import PaymentForm from './PaymentForm';
import { EPlacementKeys } from '@/api/resources/Paywall';
function PaymentModalV1(props: HTMLAttributes<HTMLDivElement>) {
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
@ -21,9 +20,6 @@ function PaymentModalV1(props: HTMLAttributes<HTMLDivElement>) {
>
<PaymentForm
activeProduct={activeProductFromStore}
placementKey={
EPlacementKeys["aura.placement.palmistry.redesign"]
}
/>
</div>
</div>

View File

@ -34,12 +34,12 @@ function PaymentTable({
};
const getPrice = useCallback(
(product: IPaywallProduct) => {
if ((product.trialPrice || 0) % 100 === 0) {
return addCurrency((product.trialPrice || 0) / 100, currency);
(price: number) => {
if (price % 100 === 0) {
return addCurrency(price / 100, currency);
}
return addCurrency(
((product.trialPrice || 0) / 100).toFixed(2),
(price / 100).toFixed(2),
currency
);
},
@ -78,13 +78,13 @@ function PaymentTable({
{/* {translate("/trial-payment.payment_table.title", {
price: <span className={styles.purple}>{getPrice(product)}</span>,
})} */}
Personalized plan for <span className={styles.blue}>{getPrice(product)}</span>
Personalized plan for <span className={styles.blue}>{getPrice(product.trialPrice || 0)}</span>
</Title>
<div className={styles["table-element"]}>
<p className={styles["total-today"]}>
{translate("/trial-payment.payment_table.total_today")}
</p>
<span>{getPrice(product)}</span>
<span>{getPrice(product.trialPrice || 0)}</span>
</div>
<hr />
<div className={styles["table-element"]}>
@ -109,9 +109,9 @@ function PaymentTable({
)} */}
<div>
<span className={styles.discount}>
{addCurrency(Number(getText("full.price")) / 100, currency)}
{addCurrency(Number(getText("full.price")), currency)}
</span>
<span className={styles["discount-price"]}>{getPrice(product)}</span>
<span className={styles["discount-price"]}>{getPrice(product.price)}</span>
</div>
</div>
</div>
@ -129,7 +129,7 @@ function PaymentTable({
),
trialDuration: product.trialDuration,
price: addCurrency(product.price / 100, currency),
trialPrice: getPrice(product),
trialPrice: getPrice(product.trialPrice || 0),
})}
</p>
</>

View File

@ -1,8 +1,34 @@
import Title from "@/components/Title";
import styles from "./styles.module.scss";
import { images } from "../../data";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { addCurrency, ELocalesPlacement } from "@/locales";
import { EPlacementKeys } from "@/api/resources/Paywall";
import { Currency } from "@/components/PaymentTable";
const placementKey = EPlacementKeys["aura.placement.email.palmistry.discount"]
const getPrice = (price: number, currency: Currency) => {
if (price % 100 === 0) {
return addCurrency(price / 100, currency);
}
return addCurrency(
(price / 100).toFixed(2),
currency
);
}
function SecretDiscountTable() {
const { products, currency, getText } = usePaywall({
placementKey,
localesPlacement: ELocalesPlacement.PalmistryV1,
});
const activeProduct = products[0];
const price = activeProduct?.price || 0;
const trialPrice = activeProduct?.trialPrice || 0;
const trialDuration = activeProduct?.trialDuration || 7;
return (
<div className={styles.container}>
<Title className={styles.title} variant="h3">
@ -14,21 +40,21 @@ function SecretDiscountTable() {
<Title className={styles.title} variant="h4">
Secret discount applied!
</Title>
<span className={styles["old-discount"]}>-30%</span>
<span className={styles["new-discount"]}>-50%</span>
<span className={styles["old-discount"]}>{getText("old.discount")}</span>
<span className={styles["new-discount"]}>{getText("new.discount")}</span>
</div>
<div className={`${styles["grid-line"]} ${styles["days-14"]}`}>
<p>Your cost per 14 days after trial:</p>
<span className={styles["old-price"]}>$19</span>
<span className={styles["new-price"]}>$19</span>
<p>Your cost per {trialDuration} days after trial:</p>
<span className={styles["old-price"]}>{addCurrency(Number(getText("old.price")), currency)}</span>
<span className={styles["new-price"]}>{getPrice(price, currency)}</span>
</div>
<div className={`${styles["grid-line"]} ${styles["save"]}`}>
<p>You save $30</p>
<p>You save {addCurrency(Number(getText("save")), currency)}</p>
</div>
<hr />
<div className={`${styles["grid-line"]} ${styles["total-today"]}`}>
<p>Total today</p>
<span>$9</span>
<span>{getPrice(trialPrice, currency)}</span>
</div>
</div>
)

View File

@ -10,9 +10,11 @@ import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall";
import { ELocalesPlacement } from "@/locales";
const placementKey = EPlacementKeys["aura.placement.email.palmistry.discount"]
function SaveOff() {
const { products } = usePaywall({
placementKey: EPlacementKeys["aura.placement.email.marketing"],
const { products, getText } = usePaywall({
placementKey,
localesPlacement: ELocalesPlacement.PalmistryV1,
});
const activeProduct = products[0]
@ -31,16 +33,16 @@ function SaveOff() {
<Blob2 className={styles.blob2} />
<img className={styles.gift} src={images("gift.svg")} alt="gift" />
<Title className={styles.title}>
SAVE 70% OFF!
SAVE {getText("discount")}% OFF!
</Title>
<p className={styles.description}>
<span className={styles.price}>${price}</span> instead <span className={styles.discount}>of $65</span>
<span className={styles.price}>${price}</span> instead <span className={styles.discount}>of ${getText("full.price")}</span>
</p>
<p className={styles.point} style={{ marginTop: 12 }}>
<img src={images("fire.png")} alt="fire" /> {trialDuration}-day trial
</p>
<p className={styles.point}>
<img src={images("gift.png")} alt="gift" /> 70% off on your personalized plan
<img src={images("gift.png")} alt="gift" /> {getText("discount")}% off on your personalized plan
</p>
<Button className={styles.button} onClick={handleNext}>
GET {trialDuration}-day trial

View File

@ -8,53 +8,84 @@ import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall";
import { ELocalesPlacement } from "@/locales";
import Modal from "@/components/Modal";
import PaymentModal from "@/components/PaymentModal";
import { useState } from "react";
import { useEffect, useState } from "react";
import PaymentForm from "@/components/Payment/nmi/PaymentForm";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
import { useDispatch } from "react-redux";
import { actions } from "@/store";
const placementKey = EPlacementKeys["aura.placement.email.palmistry.discount"]
function SecretDiscount() {
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
const dispatch = useDispatch();
const navigate = useNavigate();
const { products } = usePaywall({
placementKey: EPlacementKeys["aura.placement.email.marketing"],
placementKey,
localesPlacement: ELocalesPlacement.PalmistryV1,
});
const activeProduct = products[0]
const price = (activeProduct?.price || 0) / 100
const trialDuration = activeProduct?.trialDuration || 7
const openStripeModal = () => {
setIsOpenPaymentModal(true);
};
const activeProduct = products[0];
const price = (activeProduct?.price || 0) / 100;
const trialDuration = activeProduct?.trialDuration || 7;
const handleCloseModal = () => {
setIsOpenPaymentModal(false);
useEffect(() => {
if (!activeProduct) return;
dispatch(actions.payment.update({
activeProduct
}))
}, [activeProduct])
const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
const onPaymentSuccess = () => {
return navigate(routes.client.paymentSuccess())
}
const onModalClosed = () => {
setIsPaymentModalOpen(false);
}
const onPaymentError = () => {
return navigate(routes.client.paymentFail())
}
const openPaymentModal = () => {
setIsPaymentModalOpen(true);
};
return (
<>
{products[0] && (
{activeProduct && (
<Modal
containerClassName={styles.modal}
open={isOpenPaymentModal}
onClose={handleCloseModal}
open={isPaymentModalOpen}
onClose={onModalClosed}
type="hidden"
>
<PaymentModal
placementKey={EPlacementKeys["aura.placement.email.marketing"]}
<PaymentForm
placementKey={placementKey}
onPaymentError={onPaymentError}
onPaymentSuccess={onPaymentSuccess}
/>
</Modal>
)}
<Blob3 className={styles.blob3} />
<Blob4 className={styles.blob4} />
<Title className={styles.title} variant="h1">
You get a secret discount!
</Title>
<SecretDiscountTable />
<Button className={styles.button} onClick={openStripeModal}>
<Button className={styles.button} onClick={openPaymentModal}>
GET {trialDuration}-DAY TRIAL
</Button>
<p className={styles.policy}>
By continuing you agree that if you don't cancel prior to the end of the {trialDuration}-days trial, you will automatically be charged ${price} for the introductory period of 14 days thereafter the standard rate of ${price} every 14 days until you cancel in settings. Learn more about cancellation and refund policy in Subscription terms.
</p>
<div className={styles["policy-container"]}>
<p className={styles.policy}>
By continuing you agree that if you don't cancel prior to the end of the {trialDuration}-days trial, you will automatically be charged ${price} for the introductory period of 14 days thereafter the standard rate of ${price} every 14 days until you cancel in settings. Learn more about cancellation and refund policy in Subscription terms.
</p>
<Blob4 className={styles.blob4} />
</div>
</>
)
}

View File

@ -5,13 +5,6 @@
z-index: -1;
}
.blob4 {
position: absolute;
bottom: 0;
left: 0;
z-index: -1;
}
.title {
padding: 15px 0;
background-color: #F096C4;
@ -33,16 +26,26 @@
margin-top: 30px;
}
.policy {
width: 100%;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 0;
margin-bottom: 34px;
padding: 0 14px;
font-size: 13px;
font-weight: 400;
line-height: 130%;
color: #fff;
.policy-container {
position: relative;
width: calc(100% + 52px);
height: fit-content;
bottom: -58px;
&>.policy {
width: 100%;
margin: 34px 0;
padding: 0 14px;
font-size: 13px;
font-weight: 400;
line-height: 130%;
color: #fff;
}
.blob4 {
position: absolute;
bottom: 0;
left: 0;
z-index: -1;
}
}

View File

@ -8,26 +8,32 @@ import { ELocalesPlacement } from "@/locales";
import { images } from "../../data";
import DiscountExpires from "../../components/DiscountExpires";
import PaymentTable from "../../components/PaymentTable";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store";
import { EPlacementKeys } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import MoneyBackGuarantee from "../../components/MoneyBackGuarantee";
import PalmsSayAbout from "../../components/PalmsSayAbout";
import { useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import { getZodiacSignByDate } from "@/services/zodiac-sign";
import WithPartnerInformation from "../../components/WithPartnerInformation";
import PersonalInformation from "../../components/PersonalInformation";
import Reviews from "../../components/Reviews";
import Address from "../../components/Address";
import Modal from "@/components/Modal";
import PaymentForm from "@/components/Payment/nmi/PaymentForm";
const placementKey = EPlacementKeys["aura.placement.email.palmistry"];
function TrialPayment() {
const dispatch = useDispatch();
// const { translate } = useTranslations(ELocalesPlacement.PalmistryV1);
const { products } = usePaywall({
placementKey: EPlacementKeys["aura.placement.email.marketing"],
placementKey,
localesPlacement: ELocalesPlacement.PalmistryV1,
});
const activeProduct = products[0]
const trialDuration = activeProduct?.trialDuration || 7;
const birthdate = useSelector(selectors.selectBirthdate);
@ -43,6 +49,28 @@ function TrialPayment() {
const partnerZodiacSign = getZodiacSignByDate(partnerBirthdate);
const navigate = useNavigate();
const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
const onPaymentSuccess = () => {
return navigate(routes.client.paymentSuccess())
}
const onModalClosed = () => {
setIsPaymentModalOpen(false);
return handleDiscount()
}
const handleDiscount = () => {
navigate(routes.client.palmistryV2SaveOff());
};
const onPaymentError = () => {
return navigate(routes.client.paymentFail())
}
const openPaymentModal = () => {
setIsPaymentModalOpen(true);
};
const singleOrWithPartner = useMemo(() => {
if (["relationship", "married"].includes(flowChoice)) {
@ -51,14 +79,25 @@ function TrialPayment() {
return "single";
}, [flowChoice]);
const handleNext = () => {
navigate(routes.client.palmistryV2SaveOff());
};
useEffect(() => {
if (!activeProduct) return;
dispatch(actions.payment.update({
activeProduct
}))
}, [activeProduct])
if (!activeProduct) return null;
return (
<>
<Modal containerClassName={styles.modal} open={isPaymentModalOpen} onClose={onModalClosed}>
<PaymentForm
placementKey={placementKey}
onPaymentError={onPaymentError}
onPaymentSuccess={onPaymentSuccess}
/>
</Modal>
<div className={styles.background} />
<div className={styles.header}>
<Title className={styles.title}>
@ -71,7 +110,7 @@ function TrialPayment() {
</div>
<div className={styles.discount}>
<DiscountExpires />
<Button className={styles.button} onClick={handleNext}>
<Button className={styles.button} onClick={openPaymentModal}>
GET {trialDuration}-day trial
</Button>
</div>
@ -82,8 +121,8 @@ function TrialPayment() {
<PaymentTable
product={activeProduct}
gender={gender}
placementKey={EPlacementKeys["aura.placement.email.marketing"]}
buttonClick={handleNext}
placementKey={placementKey}
buttonClick={openPaymentModal}
/>
<MoneyBackGuarantee />
<Title className={styles["title-hands"]}>

View File

@ -33,7 +33,7 @@
.discount {
position: sticky;
top: 0;
top: -1px;
width: calc(100% + 48px);
padding: 9px 22px;
border: solid 1px #fff;
@ -42,8 +42,9 @@
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: #dae7ff;
z-index: 9999;
background-color: transparent;
backdrop-filter: blur(5px);
z-index: 1000;
&>.button {
max-width: 176px;

View File

@ -0,0 +1,134 @@
import MainButton from "@/components/MainButton";
import { useEffect } from "react";
import styles from "./styles.module.scss";
import { usePayment } from "@/hooks/payment/nmi/usePayment";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
export type TConfirmType = "payment" | "setup";
interface ICheckoutFormProps {
subscriptionReceiptId?: string;
returnUrl?: string;
confirmType?: TConfirmType;
isHide?: boolean;
placementKey: EPlacementKeys;
activeProduct: IPaywallProduct;
onSuccess?: () => void;
onError?: (error?: string) => void;
onModalClosed?: () => void;
}
export default function CheckoutForm({
placementKey,
activeProduct,
onError,
onSuccess,
onModalClosed,
isHide = false,
}: ICheckoutFormProps) {
const {
isLoading,
error,
isPaymentSuccess,
submitInlineForm,
formValidation,
isFormValid,
isModalClosed
} = usePayment({
placementKey,
activeProduct,
paymentFormType: "inline"
});
useEffect(() => {
if (error && onError) {
console.log(error);
onError(error);
}
}, [error, onError]);
useEffect(() => {
if (isModalClosed && onModalClosed) {
onModalClosed();
}
}, [isModalClosed, onModalClosed]);
useEffect(() => {
if (isPaymentSuccess && onSuccess) {
onSuccess();
}
}, [isPaymentSuccess, onSuccess]);
const handleSubmit = (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!isFormValid) {
return;
}
submitInlineForm();
};
return (
<form
className={`${styles.form} ${isHide ? styles.hide : ""}`}
id="payment-form"
onSubmit={handleSubmit}
>
<div className={styles.formRow}>
<label htmlFor="card-number">Card Number</label>
<div
id="card-number"
className={`${styles.fieldContainer} ${formValidation.ccnumber.message ? styles.invalid : ''
} ${formValidation.ccnumber.isValid ? styles.valid : ''}`}
/>
{formValidation.ccnumber.message && (
<div className={styles.errorMessage}>
{formValidation.ccnumber.message}
</div>
)}
</div>
<div className={styles.formGroup}>
<div className={styles.formRow}>
<label htmlFor="card-expiry">Expiration Date</label>
<div
id="card-expiry"
className={`${styles.fieldContainer} ${formValidation.ccexp.message ? styles.invalid : ''
} ${formValidation.ccexp.isValid ? styles.valid : ''}`}
/>
{formValidation.ccexp.message && (
<div className={styles.errorMessage}>
{formValidation.ccexp.message}
</div>
)}
</div>
<div className={styles.formRow}>
<label htmlFor="card-cvv">CVV</label>
<div
id="card-cvv"
className={`${styles.fieldContainer} ${formValidation.cvv.message ? styles.invalid : ''
} ${formValidation.cvv.isValid ? styles.valid : ''}`}
/>
{formValidation.cvv.message && (
<div className={styles.errorMessage}>
{formValidation.cvv.message}
</div>
)}
</div>
</div>
<MainButton
color="blue"
disabled={isLoading || !isFormValid}
id="submit"
className={styles.button}
onClick={handleSubmit}
>
<img src="/lock.svg" alt="Secure" />
<span>{isLoading ? "Processing..." : "Pay Now"}</span>
</MainButton>
</form>
);
}

View File

@ -0,0 +1,134 @@
.page {
/* position: relative; */
position: static;
/* height: calc(100vh - 50px);
max-height: -webkit-fill-available; */
display: flex;
justify-items: center;
justify-content: center;
gap: 16px;
}
.hide {
height: 0;
visibility: hidden;
}
.payment-loader {
display: flex;
justify-content: center;
align-items: center;
}
.cross {
position: absolute;
top: -36px;
right: 28px;
width: 22px;
height: 22px;
cursor: pointer;
z-index: 9;
}
.title {
font-size: 27px;
font-weight: 700;
margin: 0;
}
.email {
font-size: 17px;
font-weight: 500;
margin: 0;
}
.button {
min-height: 0;
height: 50px;
text-transform: uppercase;
background-color: #4caf50;
border-radius: 25px;
font-size: 16px;
gap: 8px;
opacity: 1;
transition: opacity 0.2s ease;
margin-top: 8px;
&:disabled {
opacity: 0.7;
cursor: not-allowed;
}
}
.button>img {
height: 18px;
margin-top: -5px;
}
.form {
width: 100%;
max-width: 500px;
margin: 0 auto;
padding: 0;
}
.hide {
display: none;
}
.formRow {
margin-bottom: 16px;
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
text-align: left;
padding-left: 12px;
}
}
.formGroup {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.input {
width: 100%;
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.2s;
&:focus {
outline: none;
border-color: #3b82f6;
}
}
.fieldContainer {
border: 1px solid #e2e8f0;
border-radius: 6px;
overflow: hidden;
transition: border-color 0.2s;
padding: 8px;
min-height: 40px;
&:focus-within {
border-color: #3b82f6;
}
&.invalid {
border-color: #dc3545;
}
}
.errorMessage {
color: #dc3545;
font-size: 12px;
margin: 4px 0;
min-height: 20px;
}

View File

@ -0,0 +1,97 @@
import { useTranslations } from "@/hooks/translations";
import styles from "./styles.module.scss";
import { addCurrency, ELocalesPlacement } from "@/locales";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
import Loader from "@/components/Loader";
import Title from "@/components/Title";
import PaymentMethodsChoice from "@/components/pages/ABDesign/v1/pages/TrialPayment/components/PaymentMethodsChoice";
import { paymentMethods } from "@/data/paymentMethods";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import SecurityPayments from "@/components/pages/ABDesign/v1/pages/TrialPayment/components/SecurityPayments";
import CheckoutForm from "../CheckoutForm";
const paymentMethodsButtons = paymentMethods(null);
const getPrice = (product: IPaywallProduct | null) => {
if (!product) {
return 0;
}
return (product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100;
}
interface IPaymentFormProps {
className?: string;
placementKey: EPlacementKeys;
onPaymentError?: () => void;
onPaymentSuccess?: () => void;
onModalClosed?: () => void;
}
function PaymentForm({ className, placementKey, onPaymentError, onPaymentSuccess, onModalClosed }: IPaymentFormProps) {
const { translate } = useTranslations(ELocalesPlacement.V1);
const currency = useSelector(selectors.selectCurrency);
const activeProduct = useSelector(selectors.selectActiveProduct);
const isLoading = false;
return (
<>
{isLoading && (
<div className={styles["payment-modal"]}>
<div className={styles["payment-loader"]}>
<Loader />
</div>
</div>
)}
<div
className={`${styles["payment-modal"]} ${isLoading ? styles.hide : ""} ${className}`}
>
<Title variant="h3" className={styles.title}>
{translate("payment_modal.title")}
</Title>
<PaymentMethodsChoice
paymentMethods={paymentMethodsButtons}
selectedPaymentMethod={paymentMethodsButtons[0].id}
onSelectPaymentMethod={() => { }}
/>
{activeProduct && (
<div>
<p className={styles["sub-plan-description"]}>
{translate("payment_modal.description", {
priceForDays: (
<b>
{translate("payment_modal.price_for_days", {
trialPrice: addCurrency(
getPrice(activeProduct),
currency
),
trialDuration: activeProduct?.trialDuration,
})}
</b>
),
emailReminder: (
<b>{translate("payment_modal.email_reminder")}</b>
),
})}
</p>
</div>
)}
<div className={styles["payment-method-container"]}>
{!!activeProduct && <CheckoutForm
placementKey={placementKey}
activeProduct={activeProduct}
onError={onPaymentError}
onSuccess={onPaymentSuccess}
onModalClosed={onModalClosed}
/>}
</div>
<SecurityPayments />
<p className={styles.address}>{translate("payment_modal.address")}</p>
</div>
</>
)
}
export default PaymentForm

View File

@ -0,0 +1,55 @@
.payment-modal {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 250px;
gap: 25px;
color: #2f2e37;
}
.payment-modal.hide {
min-height: 0;
height: 0;
opacity: 0;
}
.title {
font-weight: 700;
font-size: 20px;
line-height: 20px;
text-align: center;
margin: 0;
}
.sub-plan-description {
font-size: 12px;
text-align: center;
line-height: 150%;
white-space: pre-wrap;
}
.payment-method-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 24px;
}
.address {
margin-bottom: 24px;
text-transform: uppercase;
}
.payment-method {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
}
.address {
color: gray;
font-size: 10px;
}

View File

@ -1,5 +1,5 @@
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
// import { useNavigate } from "react-router-dom";
// import routes from "@/routes";
import styles from "./styles.module.css";
import UserHeader from "../UserHeader";
import { useDispatch, useSelector } from "react-redux";
@ -16,7 +16,7 @@ import { getRandomArbitrary } from "@/services/random-value";
function PriceListPage(): JSX.Element {
const { translate } = useTranslations();
const navigate = useNavigate();
// const navigate = useNavigate();
const dispatch = useDispatch();
const homeConfig = useSelector(selectors.selectHome);
const selectedPrice = useSelector(selectors.selectSelectedPrice);
@ -41,7 +41,7 @@ function PriceListPage(): JSX.Element {
})
);
setTimeout(() => {
navigate(routes.client.subscription());
// navigate(routes.client.subscription());
}, 1000);
};

View File

@ -1,92 +0,0 @@
import { useNavigate } from "react-router-dom";
import { useTranslations } from "@/hooks/translations";
import Title from "../Title";
import routes from "@/routes";
import styles from "./styles.module.css";
import ModalTop from "../ModalTop";
import Header from "../Header";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store";
import MainButton from "../MainButton";
interface ModalTopProps {
open: boolean;
onClose?: () => void;
}
function SpecialWelcomeOffer({ open, onClose }: ModalTopProps): JSX.Element {
const { translate } = useTranslations();
const navigate = useNavigate();
const dispatch = useDispatch();
const selectedPrice = useSelector(selectors.selectSelectedPrice);
const halfPrice = (Math.round(selectedPrice || 0) / 2).toFixed(2);
const updateIsDiscount = useCallback((isDiscount: boolean) => {
dispatch(
actions.payment.update({
isDiscount
})
);
}, [dispatch]);
const handleNext = () => {
updateIsDiscount(true);
navigate(routes.client.paymentMethod());
};
const handleMoreAbout = () => {
window.location.href = "https://witapps.us/en/aura";
};
return (
<>
{open ? (
<ModalTop open={open} onClose={onClose || handleNext}>
<Header
showBack={false}
showCross={false}
clickCross={onClose || handleNext}
/>
<div className={styles.content}>
{/* <span className={styles['welcome-offer']}>{translate('special_welcome_offer')}</span> */}
<img src="/your-friends.webp" alt="Your friends" />
<Title variant="h2" className={styles["your-friends"]}>
{translate("au.friends.window")}
</Title>
<Title variant="h2" className={styles["get-50-only"]}>
{translate("au.get50.only")}
</Title>
<div className={styles["discount-container"]}>
{Number(halfPrice) > 0 &&
<>
<span className={styles["red-price"]}>${selectedPrice}</span>{" "}
<span className={styles["price"]}>
{/* ${halfPrice} */}
$1
</span>
</>
}
{!Number(halfPrice) && <span className={styles["free-trial"]}>{translate('au.free_trial_web.7_14')}</span>}
</div>
<MainButton
className={styles["button-green"]}
onClick={handleNext}
>
{/* $ {halfPrice} */}
{translate("au.try_for.button")}
</MainButton>
<MainButton
// disabled
className={styles["button-black"]}
onClick={handleMoreAbout}
>
<img className={styles["button-icon"]} src="/leo.webp" alt="Leo" />
{translate("au.more_llc.button")}
</MainButton>
</div>
</ModalTop>
) : null}
</>
);
}
export default SpecialWelcomeOffer;

View File

@ -1,73 +0,0 @@
.content {
padding: 0 24px 64px 24px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding-top: 28px;
}
.welcome-offer {
color: #717171;
font-weight: 500;
}
.discount-container {
display: flex;
flex-direction: row;
gap: 16px;
font-size: 32px;
font-weight: 600;
margin-bottom: 32px;
}
.red-price {
color: red;
text-decoration: line-through;
}
.button-green {
background-color: #18d136;
font-weight: 600;
font-size: 21px;
/* color: #fff;
border-radius: 26px;
width: 100%;
max-width: 300px;
padding: 12px 0;
border: none;
font-size: 14px;
margin-top: 16px; */
}
.button-black {
font-weight: 600;
font-size: 21px;
position: relative;
}
.your-friends {
width: 80%;
font-weight: 600;
margin-bottom: 8px;
}
.get-50-only {
margin-bottom: 0;
font-weight: 700;
color: #ff003d;
}
.button-icon {
position: absolute;
width: 48px;
top: 50%;
left: 13px;
transform: translateY(-50%);
}
.free-trial {
font-size: 22px;
font-weight: 600;
}

View File

@ -1,36 +0,0 @@
import { useParams } from "react-router";
import { useTranslations } from "@/hooks/translations";
import { useApi, useApiCall, Element } from "@/api";
import { useCallback } from "react";
import parse from "html-react-parser";
import Loader from "../Loader";
import NotFoundPage from "../NotFoundPage";
import "./styles.css";
function StaticPage(): JSX.Element {
const { i18n } = useTranslations();
const { typeId } = useParams();
const api = useApi();
const locale = i18n.language;
const loadData = useCallback(() => {
const type = typeId || "";
return api
.getElement({ type, locale })
.then((resp: Element.Response) => resp.data.element);
}, [api, typeId, locale]);
const { data, isPending, error } = useApiCall<Element.Element>(loadData);
const content = data ? parse(data.body) : null;
return (
<section className="page page-static">
{isPending ? (
<Loader />
) : (
<div className="page-static__content">{content}</div>
)}
{error && <NotFoundPage />}
</section>
);
}
export default StaticPage;

View File

@ -1,18 +0,0 @@
.page-static {
line-height: 1.3;
}
.page-static p {
margin-bottom: 5px;
}
.page-static h1,
.page-static h2,
.page-static h3 {
margin-top: 10px;
margin-bottom: 10px;
}
.page-static__content {
width: 100%;
}

View File

@ -1,272 +0,0 @@
import { useDispatch, useSelector } from "react-redux";
import { useTranslations } from "@/hooks/translations";
import { useNavigate, useParams } from "react-router-dom";
import { actions, selectors } from "@/store";
import MainButton from "../MainButton";
import Policy from "../Policy";
import PaymentTable, { Currency, Locale } from "../PaymentTable";
import CallToAction from "../CallToAction";
import routes from "@/routes";
import styles from "./styles.module.css";
// import Header from "../Header";
// import SpecialWelcomeOffer from "../SpecialWelcomeOffer";
import { useEffect, useState } from "react";
import { ApiError, extractErrorMessage, useApi } from "@/api";
import { useAuth } from "@/auth";
import { getClientTimezone, language } from "@/locales";
import Loader from "../Loader";
import Title from "../Title";
import ErrorText from "../ErrorText";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall";
const currency = Currency.USD;
const locale = language as Locale;
const getPrice = (product: IPaywallProduct | null) => {
if (!product?.trialPrice) {
return 0;
}
return (product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100;
};
function SubscriptionPage(): JSX.Element {
const api = useApi();
const timezone = getClientTimezone();
const { signUp } = useAuth();
const { translate } = useTranslations();
const navigate = useNavigate();
const dispatch = useDispatch();
// const [isOpenModal, setIsOpenModal] = useState(false);
const [email, setEmail] = useState("");
const [emailError, setEmailError] = useState<null | string>(
"Email is invalid"
);
const [name, setName] = useState("");
const [nameError, setNameError] = useState<null | string>("Name is invalid");
const [isSubmit, setIsSubmit] = useState(false);
const [isAuth, setIsAuth] = useState(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [apiError, setApiError] = useState<ApiError | null>(null);
const [error, setError] = useState<boolean>(false);
const { subPlan } = useParams();
const birthday = useSelector(selectors.selectBirthday);
console.log(nameError);
const { products } = usePaywall({
placementKey: EPlacementKeys["aura.placement.main"],
});
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
const [activeProduct, setActiveProduct] = useState<IPaywallProduct | null>(
activeProductFromStore
);
useEffect(() => {
if (subPlan) {
const targetProduct = products.find(
(product) =>
String(
product?.trialPrice
? Math.floor((product?.trialPrice + 1) / 100)
: product.key.replace(".", "")
) === subPlan
);
if (targetProduct) {
setActiveProduct(targetProduct);
}
}
}, [products, subPlan]);
const paymentItems = [
{
title: activeProduct?.name || "Per 7-Day Trial For",
price: getPrice(activeProduct),
description: activeProduct?.description?.length
? activeProduct.description
: translate("au.2week_plan.web"),
},
];
const authorization = async () => {
try {
setIsLoading(true);
const auth = await api.auth({ email, timezone, locale });
const {
auth: { token, user },
} = auth;
signUp(token, user);
const payload = {
user: { profile_attributes: { birthday } },
token,
};
const updatedUser = await api.updateUser(payload).catch((error) => {
console.log("Error: ", error);
});
if (updatedUser?.user) {
dispatch(actions.user.update(updatedUser.user));
}
if (name) {
dispatch(
actions.user.update({
username: name,
})
);
}
dispatch(actions.status.update("registred"));
dispatch(
actions.payment.update({
activeProduct,
})
);
setIsLoading(false);
setIsAuth(true);
setTimeout(() => {
navigate(routes.client.paymentMethod());
}, 1000);
} catch (error) {
console.error(error);
if (error instanceof ApiError) {
setApiError(error as ApiError);
} else {
setError(true);
}
setIsLoading(false);
}
};
const handleClick = async () => {
setIsSubmit(true);
if (
!isValidEmail(email)
// || !isValidName(name)
) {
return;
}
await authorization();
};
// const handleCross = () => setIsOpenModal(true);
const policyLink = (
<a href="https://aura.wit.life/" target="_blank" rel="noopener noreferrer">
{translate("subscription_policy")}
</a>
);
const isValidEmail = (email: string) => {
return /\S+@\S+\.\S+/.test(email);
};
const isValidName = (name: string) => {
return !!(name.length > 0 && name.length < 30);
};
const handleChangeEmail = (event: React.ChangeEvent<HTMLInputElement>) => {
const email = event.target.value;
if (!isValidEmail(email)) {
setEmailError("Email is invalid");
} else {
setEmailError(null);
}
setEmail(email);
};
const handleChangeName = (event: React.ChangeEvent<HTMLInputElement>) => {
const name = event.target.value;
if (!isValidName(name)) {
setNameError("Name is invalid");
} else {
setNameError(null);
}
setName(name);
};
return (
<>
{/* <SpecialWelcomeOffer open={isOpenModal} onClose={handleClick} /> */}
{/* <Header classCross={styles.cross} clickCross={handleCross} /> */}
{/* <UserHeader email={email} /> */}
<section className={`${styles.page} page`}>
<CallToAction />
{/* <Countdown start={10} /> */}
<PaymentTable
items={paymentItems}
currency={currency}
locale={locale}
/>
<div className={styles["inputs-container"]}>
<div className={styles["inputs-container__input-container"]}>
<input
className={`${styles["inputs-container__input"]}`}
// ${
// nameError && isSubmit && styles["inputs-container__input-error"]
// }
type="name"
name="name"
id="name"
value={name}
onChange={handleChangeName}
placeholder="Your name"
/>
{/* {isSubmit && !!nameError && (
<span className={styles["inputs-container__label-error"]}>
{nameError}
</span>
)} */}
</div>
<div className={styles["inputs-container__input-container"]}>
<input
className={`${styles["inputs-container__input"]} ${
emailError &&
isSubmit &&
styles["inputs-container__input-error"]
}`}
type="email"
name="email"
id="email"
value={email}
onChange={handleChangeEmail}
placeholder="Your email"
/>
{isSubmit && !!emailError && (
<span className={styles["inputs-container__label-error"]}>
{emailError}
</span>
)}
</div>
{isLoading && isSubmit && isValidEmail(email) && (
// isValidName(name) &&
<Loader />
)}
{(error || apiError) && (
<Title variant="h3" style={{ color: "red", margin: 0 }}>
Something went wrong
</Title>
)}
{apiError && (
<ErrorText
size="medium"
isShown={Boolean(apiError)}
message={apiError ? extractErrorMessage(apiError) : null}
/>
)}
{!apiError && !error && !isLoading && isAuth && (
<img src="/SuccessIcon.webp" alt="Success Icon" />
)}
</div>
<div className={styles["subscription-action"]}>
<MainButton onClick={handleClick}>
Start ${getPrice(activeProduct || null)}
</MainButton>
</div>
<Policy>
<>
{translate("auweb.agree.text1")}
{translate("subscription_text", { policyLink })}
</>
</Policy>
</section>
</>
);
}
export default SubscriptionPage;

View File

@ -1,59 +0,0 @@
.page {
padding-bottom: 32px !important;
}
.subscription-action {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
background-color: transparent;
padding: 15px;
}
.cross {
left: 28px;
}
.inputs-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
margin-top: 16px;
}
.inputs-container__input {
width: 100%;
border: solid #000 2px;
border-radius: 20px;
font-size: 17px;
font-weight: 400;
line-height: 20px;
padding: 16px 25px;
}
.inputs-container__input-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
}
.inputs-container__input-error {
border-color: red;
}
.inputs-container__label-error {
font-size: 12px;
font-weight: 400;
color: red;
width: 100%;
padding-left: 25px;
}

View File

@ -26,119 +26,9 @@ import { useTranslations } from "@/hooks/translations";
import {
addCurrency,
ELocalesPlacement,
getDefaultLocaleByLanguage,
language,
} from "@/locales";
import DiscountExpires from "../TrialPayment/components/DiscountExpires";
const textVariants = [
<>
AURA is the only accurate app with reliable astrological predictions,
verified by professionals and guaranteed to provide accurate astrological
forecasts.
<br />
<br />
AURA has already helped millions of people find happiness and discover the
whole truth about their relationships.
<br />
<br />
An astrological forecast that will completely change your life is almost
ready! Before we provide it to you, we would like to offer you the
opportunity to choose the amount you consider reasonable to try AURA for 7
days and which you think is fair for the changes that will happen to you:
<br />
<br />
- You will discover all the most intimate secrets that the stars have
prepared for you and solve relationship issues within just one month;
<br />
- You will once and for all put the finishing touches on unresolved issues
and forget about problems that have been haunting you for years (if not
decades);
<br />
- You will save hundreds of dollars on fake and unprofessional astrological
predictions and fortune tellers;
<br />
- You will receive not only a personal astrological forecast but also
personalized daily horoscopes, learn who and how is draining your energy,
and get other personalized readings.
<br />
<br />
<span className={`${styles.text} ${styles.bold} ${styles.blue}`}>
A 7-day trial period costs us $5, but please choose the amount that suits
you best:
</span>
</>,
<>
AURA is the only app you can trust for accurate astrological insights,
crafted and verified by seasoned professionals, ensuring you receive
predictions that are both reliable and transformative.
<br />
<br />
AURA has already transformed the lives of millions, bringing clarity, joy,
and a deeper understanding of their relationships.
<br />
<br />
Your life-changing astrological forecast is almost ready! But before we
reveal the secrets that will change your life, we want to give you the
freedom to choose how much you feel is fair to try AURA for 7 days. This is
your chance to decide what the transformation is worth to you:
<br />
<br />
- Uncover the deepest, most intimate secrets the stars have in store for
you, and watch your relationship issues resolve in just one month;
<br />
- Finally, put an end to those lingering issues that have been troubling you
for years, maybe even decades;
<br />
- Save hundreds of dollars by avoiding unreliable astrologers and fake
fortune tellers;
<br />
- Receive not just a personal astrological forecast but also personalized
daily horoscopes, learn who's draining your energy, and get exclusive,
tailored readings.
<br />
<br />
<span className={`${styles.text} ${styles.bold} ${styles.blue}`}>
While a 7-day trial costs us $5, we want you to choose the amount you
believe is right for you:
</span>
</>,
<>
Discover AURAthe only app that delivers truly accurate astrological
forecasts, with predictions you can trust, all verified by top industry
professionals. Your path to clarity and happiness starts here.
<br />
<br />
Millions have already found happiness and uncovered the truth about their
relationships with AURA. Now, its your turn.
<br />
<br />
Your life-changing astrological forecast is almost ready! Before we share
this powerful insight with you, were giving you the chance to set your own
price to experience AURA for 7 days. You decide what feels right for the
life-changing revelations youll receive:
<br />
<br />
- Reveal the deepest secrets the universe has in store for you and resolve
your relationship dilemmas within a month;
<br />
- Finally close the chapter on long-standing issues that have plagued you
for years, perhaps even decades;
<br />
- Avoid wasting hundreds of dollars on untrustworthy, fake astrologers;
<br />
- Gain access to your personal astrological forecast, receive daily
personalized horoscopes, learn whos been draining your energy, and benefit
from other insightful readings.
<br />
<br />
<span className={`${styles.text} ${styles.bold} ${styles.blue}`}>
Our 7-day trial typically costs us $5, but you get to choose the amount
that feels right for you:
</span>
</>,
];
enum EDisplayOptionButton {
"alwaysVisible" = "alwaysVisible",
"visibleIfChosen" = "visibleIfChosen",
@ -168,7 +58,38 @@ function TrialChoicePage() {
const { flags } = useMetricABFlags();
const isShowTimer = flags?.showTimerTrial?.[0] === "show";
const textVariant = Number(flags?.text?.[0]) || 0;
const textVariants = [
translate("/trial-choice.text_variant_1", {
br: "\n",
trialDuration: activeProduct?.trialDuration?.toString() || "7",
span: <span className={`${styles.text} ${styles.bold} ${styles.blue}`}>
{translate("/trial-choice.text_variant_1_span", {
trialDuration: activeProduct?.trialDuration?.toString() || "7",
})}
</span>,
}),
translate("/trial-choice.text_variant_2", {
br: "\n",
trialDuration: activeProduct?.trialDuration?.toString() || "7",
span: <span className={`${styles.text} ${styles.bold} ${styles.blue}`}>
{translate("/trial-choice.text_variant_2_span", {
trialDuration: activeProduct?.trialDuration?.toString() || "7",
})}
</span>,
}),
translate("/trial-choice.text_variant_3", {
br: "\n",
trialDuration: activeProduct?.trialDuration?.toString() || "7",
span: <span className={`${styles.text} ${styles.bold} ${styles.blue}`}>
{translate("/trial-choice.text_variant_3_span", {
trialDuration: activeProduct?.trialDuration?.toString() || "7",
})}
</span>,
}),
];
const textVariant = Number(flags?.text?.[0]) || 2;
const getPrice = useCallback(
(product: IPaywallProduct) => {
@ -276,7 +197,7 @@ function TrialChoicePage() {
}}
/>
)}
{(!textVariant || getDefaultLocaleByLanguage(language) !== "en") && (
{(!textVariant && (
<>
<p
className={styles.text}
@ -315,14 +236,14 @@ function TrialChoicePage() {
})}
</p>
</>
)}
{!!textVariant && getDefaultLocaleByLanguage(language) === "en" && (
))}
{!!textVariant && (
<p
className={styles.text}
style={{
marginTop: getFirstParagraphMargin(),
textAlign: "left",
whiteSpace: "pre-line",
}}
>
{textVariants[textVariant - 1]}
@ -357,8 +278,8 @@ function TrialChoicePage() {
style={
arrowLeft
? {
left: arrowLeft,
}
left: arrowLeft,
}
: {}
}
/>
@ -402,9 +323,8 @@ function TrialChoicePage() {
isActiveBlur={true}
>
<QuestionnaireGreenButton
className={`${styles.button} ${
isDisabled ? styles.disabled : ""
}`}
className={`${styles.button} ${isDisabled ? styles.disabled : ""
}`}
onClick={handleNext}
>
{getText("text.button.1", {

View File

@ -28,8 +28,8 @@ import metricService, {
} 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";
import Modal from "@/components/Modal";
import PaymentForm from "@/components/Payment/nmi/PaymentForm";
const placementKey = EPlacementKeys["aura.placement.redesign.main"]
@ -60,28 +60,20 @@ function TrialPaymentPage() {
>("single");
const { subPlan } = useParams();
const { isLoading, isModalClosed, error, isPaymentSuccess, showCreditCardForm } = usePayment({
placementKey,
activeProduct: activeProduct!
});
const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
useEffect(() => {
if (isPaymentSuccess) {
return navigate(routes.client.paymentSuccess())
}
}, [isPaymentSuccess])
const onPaymentSuccess = () => {
return navigate(routes.client.paymentSuccess())
}
useEffect(() => {
if (isModalClosed && !isPaymentSuccess && !isLoading) {
return handleDiscount()
}
}, [isModalClosed, isPaymentSuccess, isLoading])
const onModalClosed = () => {
setIsPaymentModalOpen(false);
return handleDiscount()
}
useEffect(() => {
if (error?.length && error !== "Product not found") {
return navigate(routes.client.paymentFail())
}
}, [error])
const onPaymentError = () => {
return navigate(routes.client.paymentFail())
}
useEffect(() => {
metricService.reachGoal(EGoals.AURA_TRIAL_PAYMENT_PAGE_VISIT, [
@ -137,7 +129,7 @@ function TrialPaymentPage() {
const openPaymentModal = () => {
metricService.reachGoal(EGoals.AURA_PAYMENT_METHODS_OPENED);
showCreditCardForm()
setIsPaymentModalOpen(true);
};
return (
@ -148,11 +140,13 @@ function TrialPaymentPage() {
backgroundColor: gender === "male" ? "#C1E5FF" : "#F7EBFF",
}}
>
{isLoading &&
<div className={styles["loader-container"]}>
<Loader color={LoaderColor.White} />
</div>
}
<Modal containerClassName={styles.modal} open={isPaymentModalOpen} onClose={onModalClosed}>
<PaymentForm
placementKey={placementKey}
onPaymentError={onPaymentError}
onPaymentSuccess={onPaymentSuccess}
/>
</Modal>
<div className={styles["background-top-blob-container"]}>
<BackgroundTopBlob
width={pageWidth}

View File

@ -1,6 +1,6 @@
import styles from "./styles.module.css";
import PaymentDiscountTable from "./PaymentDiscountTable";
import { useEffect } from "react";
import { useState } from "react";
import { selectors } from "@/store";
import { useSelector } from "react-redux";
import { EPlacementKeys } from "@/api/resources/Paywall";
@ -8,52 +8,49 @@ 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";
import PaymentForm from "@/components/Payment/nmi/PaymentForm";
import Modal from "@/components/Modal";
const placementKey = EPlacementKeys["aura.placement.secret.discount"];
function TrialPaymentWithDiscount() {
const navigate = useNavigate()
const { translate } = useTranslations(ELocalesPlacement.V1);
const activeProduct = useSelector(selectors.selectActiveProduct);
const currency = useSelector(selectors.selectCurrency);
const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
if (!activeProduct) {
return <Navigate to={routes.client.additionalDiscountV1()} />
}
const { isLoading, error, isPaymentSuccess, showCreditCardForm } = usePayment({
placementKey: EPlacementKeys["aura.placement.secret.discount"],
activeProduct: activeProduct!
});
const onPaymentSuccess = () => {
return navigate(routes.client.paymentSuccess())
}
const onModalClosed = () => {
setIsPaymentModalOpen(false);
}
useEffect(() => {
if (isPaymentSuccess) {
return navigate(routes.client.paymentSuccess())
}
}, [isPaymentSuccess])
useEffect(() => {
if (error?.length && error !== "Product not found") {
return navigate(routes.client.paymentFail())
}
}, [error])
const onPaymentError = () => {
return navigate(routes.client.paymentFail())
}
const openPaymentModal = () => {
showCreditCardForm()
setIsPaymentModalOpen(true);
};
return (
<DiscountLayout title={translate("/trial-payment-with-discount.title")}>
{isLoading &&
<div className={styles["loader-container"]}>
<Loader color={LoaderColor.White} />
</div>
}
<Modal containerClassName={styles.modal} open={isPaymentModalOpen} onClose={onModalClosed}>
<PaymentForm
placementKey={placementKey}
onPaymentError={onPaymentError}
onPaymentSuccess={onPaymentSuccess}
/>
</Modal>
<PaymentDiscountTable />
<div className={styles['button-wrapper']}>

View File

@ -43,18 +43,6 @@ export default function DiscountScreen() {
navigate(routes.client.palmistryPremiumBundle());
};
// React.useEffect(() => {
// (async () => {
// const { sub_plans } = await api.getSubscriptionPlans({ locale });
// const plan = sub_plans.find((plan) => plan.id === "stripe.40");
//
// if (!plan?.price_cents) return;
//
// setPrice((plan?.price_cents / 100).toFixed(2));
// })();
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, []);
React.useEffect(() => {
(async () => {
const products = await api.getSinglePaymentProducts({ token });

View File

@ -10,9 +10,28 @@ import { useSelector } from "react-redux";
interface IUsePaymentProps {
placementKey: EPlacementKeys;
activeProduct: IPaywallProduct;
paymentFormType?: "lightbox" | "inline";
cardNumberRef?: React.RefObject<HTMLDivElement>;
cardExpiryRef?: React.RefObject<HTMLDivElement>;
cardCvvRef?: React.RefObject<HTMLDivElement>;
}
export const usePayment = ({ placementKey, activeProduct }: IUsePaymentProps) => {
interface IFieldValidation {
isValid: boolean;
message: string;
}
interface IFormValidation {
ccnumber: IFieldValidation;
ccexp: IFieldValidation;
cvv: IFieldValidation;
}
export const usePayment = ({
placementKey,
activeProduct,
paymentFormType = "lightbox",
}: IUsePaymentProps) => {
const api = useApi();
const token = useSelector(selectors.selectToken);
const [isSubmitting, setIsSubmitting] = useState(false);
@ -22,6 +41,15 @@ export const usePayment = ({ placementKey, activeProduct }: IUsePaymentProps) =>
const formPrice = String((activeProduct?.trialPrice || 99) / 100);
const [isOpenModal, setIsOpenModal] = useState(false);
const [isModalClosed, setIsModalClosed] = useState(false);
const [formValidation, setFormValidation] = useState<IFormValidation>({
ccnumber: { isValid: false, message: '' },
ccexp: { isValid: false, message: '' },
cvv: { isValid: false, message: '' }
});
const isFormValid = useMemo(() => {
return Object.values(formValidation).every(field => field.isValid);
}, [formValidation]);
const updatePaymentModalState = () => {
setIsOpenModal(false);
@ -39,41 +67,53 @@ export const usePayment = ({ placementKey, activeProduct }: IUsePaymentProps) =>
});
useEffect(() => {
if (!activeProduct || !products.length) return;
const product = products.find((product) => product._id === activeProduct._id);
if (!product) {
setError("Product not found");
} else {
setError(null);
}
}, [products]);
}, [products, activeProduct]);
useEffect(() => {
if (!activeProduct) return;
window.CollectJS.configure({
variant: 'lightbox',
const config: any = {
variant: paymentFormType,
callback: (token: any) => {
finishSubmit(token);
},
paymentSelector: '#customPayButton',
primaryColor: '#066fde',
theme: "material",
price: formPrice,
fields: {
ccnumber: {
placeholder: '1234 1234 1234 1234',
selector: '#ccnumber'
selector: '#card-number'
},
ccexp: {
placeholder: 'MM/YY',
selector: '#ccexp'
selector: '#card-expiry'
},
cvv: {
placeholder: 'CVC',
selector: '#cvv'
selector: '#card-cvv'
}
},
price: formPrice,
// country: "US",
// currency: "USD",
});
validationCallback: (field: string, status: boolean, message: string) => {
setFormValidation(prev => ({
...prev,
[field]: {
isValid: status,
message: status ? '' : message
}
}));
},
};
window.CollectJS?.configure(config);
}, [placementId, paywallId, activeProduct]);
const finishSubmit = async (response: any) => {
@ -106,6 +146,24 @@ export const usePayment = ({ placementKey, activeProduct }: IUsePaymentProps) =>
setIsOpenModal(true)
};
const submitInlineForm = () => {
try {
setIsSubmitting(true);
console.log("submitInlineForm");
window.CollectJS.startPaymentRequest();
} catch (error: any) {
console.error('Payment form error:', error);
setError(error?.message);
setIsSubmitting(false);
}
};
const closeModal = () => {
setIsOpenModal(false);
setIsModalClosed(true);
}
return useMemo(() => ({
isLoading,
paymentResponse,
@ -113,7 +171,11 @@ export const usePayment = ({ placementKey, activeProduct }: IUsePaymentProps) =>
isPaymentSuccess,
isOpenModal,
isModalClosed,
showCreditCardForm
showCreditCardForm,
submitInlineForm,
closeModal,
formValidation,
isFormValid
}), [
isLoading,
paymentResponse,
@ -121,6 +183,10 @@ export const usePayment = ({ placementKey, activeProduct }: IUsePaymentProps) =>
isPaymentSuccess,
isOpenModal,
isModalClosed,
showCreditCardForm
showCreditCardForm,
submitInlineForm,
closeModal,
formValidation,
isFormValid
])
}

View File

@ -692,4 +692,75 @@ export const defaultPaywalls: { [key in EPlacementKeys]: IPaywall } = {
}
]
},
"aura.placement.email.palmistry": {
"_id": "678be264aaa17756a1e517de",
"key": "aura.paywall.email.palmistry.main",
"name": "Email Palmistry",
"properties": [
{
"key": "full.price",
"value": "45",
"_id": "678be264aaa17756a1e517df"
}
],
"products": [
{
"_id": "65ff043dfc0fcfc4be550035",
"key": "compatibility.pdf.trial.0",
"productId": "prod_PnStTEBzrPLgvL",
"name": "Сompatibility AURA | Trial $0.99",
"priceId": "price_1PpFiwIlX4lgwUxruq9bpp0j",
"type": "subscription",
"description": "Description",
"discountPrice": null,
"discountPriceId": null,
"isDiscount": false,
"isFreeTrial": false,
"isTrial": true,
"price": 1900,
"trialDuration": 7,
"trialPrice": 100,
"trialPriceId": "price_1PpFoNIlX4lgwUxrP4l0lbE5",
"currency": "usd"
}
]
},
"aura.placement.email.palmistry.discount": {
"_id": "678be2b9aaa17756a1e51faa",
"key": "aura.paywall.email.palmistry.discount",
"name": "Email Palmistry Discount",
"properties": [
{
"key": "full.price",
"value": "45",
"_id": "678be264aaa17756a1e517df"
},
{
"key": "discount",
"value": "70",
"_id": ""
}
],
"products": [
{
"_id": "66589439ef0d180993cdb72f",
"key": "compatibility.secret.discount.trial.0",
"productId": "prod_PnStTEBzrPLgvL",
"name": "Сompatibility AURA Secret Discount | Trial $0.99",
"priceId": "price_1PpFlMIlX4lgwUxrUTeWDFoI",
"type": "subscription",
"description": "Description",
"discountPrice": null,
"discountPriceId": null,
"isDiscount": false,
"isFreeTrial": false,
"isTrial": true,
"price": 900,
"trialDuration": 3,
"trialPrice": 100,
"trialPriceId": "price_1PpFoNIlX4lgwUxrP4l0lbE5",
"currency": "usd"
}
]
}
}

View File

@ -16,7 +16,7 @@ export const useTranslations = (
const [searchParams] = useSearchParams();
const { t, i18n } = useTranslation();
const gender =
useSelector(selectors.selectQuestionnaire)?.gender || "default";
useSelector(selectors.selectQuestionnaire)?.gender || "male";
const { flags } = useMetricABFlags();
const esFlag = flags?.esFlag?.[0];

View File

@ -12,11 +12,9 @@ import {
buildResources,
fallbackLng,
getDefaultLocaleByLanguage,
getTranslationJSON,
ELocalesPlacement,
getTranslationsJSON,
language,
setLanguage,
TTranslationPlacements,
} from "./locales";
import App from "./components/App";
import metricService from "./services/metric/metricService";
@ -39,16 +37,10 @@ const init = async () => {
api.getAppConfig({ bundleId: "auraweb" }),
]);
const localePlacements = Object.values(ELocalesPlacement);
const translationPlacements: TTranslationPlacements = {};
for (const placement of localePlacements) {
const translationsPlacement = await getTranslationJSON(placement, language);
translationPlacements[placement] = translationsPlacement;
}
const resources = buildResources(translationsResponse, translationPlacements);
console.time('translations-loading');
const translationsPlacement = await getTranslationsJSON(language);
console.timeEnd('translations-loading');
const resources = buildResources(translationsResponse, translationsPlacement);
const legal = buildLegal(elementsResponse);
const config = configResponse.data;

View File

@ -70,58 +70,49 @@ export enum ELocalesPlacement {
PalmistryV0 = "palmistry-v0",
PalmistryV01 = "palmistry-v0_1",
PalmistryV1 = "palmistry-v1",
PalmistryV11 = "palmistry-v1_1",
Chats = "chats"
}
interface ITranslationJSON {
male: { [key: string]: string }
female: { [key: string]: string }
default: { [key: string]: string }
fallback: { male: { [key: string]: string }; female: { [key: string]: string }, default: { [key: string]: string } }
fallback: { male: { [key: string]: string }; female: { [key: string]: string } }
}
export type TTranslationPlacements = Partial<
Record<ELocalesPlacement, ITranslationJSON>
>
export const getTranslationJSON = async (placement: ELocalesPlacement | undefined, language: string): Promise<ITranslationJSON> => {
const protocol = window.location.protocol;
const host = window.location.host;
export const getTranslationsJSON = async (language: string): Promise<TTranslationPlacements> => {
const api = createApi();
let defaultLanguage = getDefaultLocaleByLanguage(language).toLowerCase();
if (defaultLanguage === "pt") {
defaultLanguage = "pt-pt"
}
const localePlacement = placement || ELocalesPlacement.V1
let result;
try {
const [
resultMale,
resultFemale,
resultMaleFallback,
resultFemaleFallback,
] = await Promise.all([
(await fetch(`${protocol}//${host}/locales/${localePlacement}/${defaultLanguage}/male_${defaultLanguage}.json`)).json(),
(await fetch(`${protocol}//${host}/locales/${localePlacement}/${defaultLanguage}/female_${defaultLanguage}.json`)).json(),
(await fetch(`${protocol}//${host}/locales/${localePlacement}/en/male_en.json`)).json(),
(await fetch(`${protocol}//${host}/locales/${localePlacement}/en/female_en.json`)).json()
]);
result = {
male: resultMale, female: resultFemale, default: resultMale, fallback: {
male: resultMaleFallback, female: resultFemaleFallback, default: resultMaleFallback
}
}
const placements = Object.values(ELocalesPlacement).filter(placement => placement !== ELocalesPlacement.V0);
try {
const responses = await Promise.all(
placements.map(place =>
api.getLocaleTranslations({
funnel: place,
locale: defaultLanguage
})
)
);
const result = responses.reduce((merged, current, index) => ({
...merged,
[placements[index]]: current
}), {});
return result;
} catch (error) {
if (language !== fallbackLng) {
result = await getTranslationJSON(localePlacement, fallbackLng)
}
result = {
male: {}, female: {}, default: {}, fallback: {
male: {}, female: {}, default: {}
}
}
console.error('Translation loading error:', error);
return {};
}
return result;
}
export const buildResources = (resp: Translations.Response, translationJSON: TTranslationPlacements) => {

View File

@ -2,11 +2,10 @@ import Header from "@/components/pages/ABDesign/v1/components/Header";
import styles from "./styles.module.scss";
import { useSchemeColorByElement } from "@/hooks/useSchemeColorByElement";
import { useEffect, useMemo, useRef } from "react";
import { Outlet, useLocation, useSearchParams } from "react-router-dom";
import { Outlet, useLocation } from "react-router-dom";
import routes from "@/routes";
import PaymentModal from "@/components/PalmistryV1/components/PaymentModal";
import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store";
import { useDispatch } from "react-redux";
import { actions } from "@/store";
const isBackButtonVisibleRoutes = [
routes.client.palmistryV1Birthdate(),
@ -40,8 +39,6 @@ const headerClassNames = {
function LayoutPalmistryV2() {
const dispatch = useDispatch();
const token = useSelector(selectors.selectToken);
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
const location = useLocation();
const mainRef = useRef<HTMLDivElement>(null);
@ -56,12 +53,6 @@ function LayoutPalmistryV2() {
useSchemeColorByElement(mainRef.current, "section.page, .page, section", [
location,
]);
const isShowPaymentModal = useSelector(
selectors.selectPalmistryIsShowPaymentModalV1
);
const [searchParams] = useSearchParams();
const subscriptionStatus =
searchParams.get("redirect_status") === "succeeded" ? "subscribed" : "lead";
const getIsBackButtonVisible = () => {
for (const route of isBackButtonVisibleRoutes) {
@ -87,19 +78,6 @@ function LayoutPalmistryV2() {
{/* <Suspense fallback={<LoadingPage />}> */}
<section className={styles.page}>
<Outlet />
{!!token.length && !!activeProductFromStore && (
// [
// routes.client.palmistryV1Payment(),
// routes.client.palmistryV1TrialPayment(),
// ].includes(location.pathname) &&
<PaymentModal
className={
isShowPaymentModal || subscriptionStatus === "subscribed"
? styles["payment-modal-active"]
: styles["payment-modal-hide"]
}
/>
)}
</section>
{/* </Suspense> */}
</main>

View File

@ -5,11 +5,15 @@
min-height: 100dvh;
max-width: 560px;
margin: 0 auto;
overflow-x: hidden;
// overflow-x: hidden;
}
.header {
padding: 8px 0 30px;
& > button {
margin-left: -12px;
}
}
.header-title {

View File

@ -7,17 +7,20 @@ const host = "";
export const apiHost = environments.AURA_API_HOST;
const dApiHost = environments.AURA_DAPI_HOST;
const dApiPrefix = environments.AURA_DAPI_PREFIX;
const siteHost = environments.AURA_SITE_HOST;
// const siteHost = environments.AURA_SITE_HOST;
const prefix = environments.AURA_PREFIX;
const openAIHost = environments.AURA_OPEN_AI_HOST;
const openAiPrefix = environments.AURA_OPEN_AI_PREFIX;
export const palmistryV1Prefix = [host, "v1", "palmistry"].join("/")
export const palmistryV2Prefix = [host, "v2", "palmistry"].join("/")
export const palmistryEmailMarketingV2Prefix = [palmistryV2Prefix, "email-marketing"].join("/")
export const emailMarketingV1Prefix = [host, "v1", "email-marketing"].join("/")
export const chatsPrefix = [host, "chats"].join("/")
export const oldBackendPrefix = [`${window.location.protocol}/`, window.location.host, "old-backend"].join("/")
const routes = {
client: {
root: () => [host, ""].join("/"),
@ -55,7 +58,7 @@ const routes = {
emailEnter: () => [host, "email"].join("/"),
authResult: () => [host, "auth", "result"].join("/"),
auth: () => [host, "auth"].join("/"),
subscription: () => [host, "subscription"].join("/"),
// subscription: () => [host, "subscription"].join("/"),
createProfile: () => [host, "profile", "create"].join("/"),
attention: () => [host, "attention"].join("/"),
feedback: () => [host, "feedback"].join("/"),
@ -64,7 +67,7 @@ const routes = {
paymentSuccess: () => [host, "payment", "success"].join("/"),
paymentFail: () => [host, "payment", "fail"].join("/"),
wallpaper: () => [host, "wallpaper"].join("/"),
static: () => [host, "static", ":typeId"].join("/"),
// static: () => [host, "static", ":typeId"].join("/"),
legal: (type: string) => [host, "static", type].join("/"),
compatibility: () => [host, "compatibility"].join("/"),
compatibilityResult: () => [host, "compatibility", "result"].join("/"),
@ -187,9 +190,9 @@ const routes = {
palmistryV1Payment: () => [palmistryV1Prefix, "payment"].join("/"),
palmistryOnboardingV1: () => [palmistryV1Prefix, "onboarding"].join("/"),
// PalmistryV2
palmistryV2TrialPayment: () => [palmistryV2Prefix, "trial-payment"].join("/"),
palmistryV2SaveOff: () => [palmistryV2Prefix, "save-off"].join("/"),
palmistryV2SecretDiscount: () => [palmistryV2Prefix, "secret-discount"].join("/"),
palmistryV2TrialPayment: () => [palmistryEmailMarketingV2Prefix, "trial-payment"].join("/"),
palmistryV2SaveOff: () => [palmistryEmailMarketingV2Prefix, "save-off"].join("/"),
palmistryV2SecretDiscount: () => [palmistryEmailMarketingV2Prefix, "secret-discount"].join("/"),
// MarketingLandingV1
emailMarketingV1Landing: () => [emailMarketingV1Prefix, "marketing-landing"].join("/"),
emailMarketingV1SpecialOffer: () => [emailMarketingV1Prefix, "special-offer"].join("/"),
@ -299,19 +302,15 @@ const routes = {
},
server: {
userLocale: () => ["https://ipapi.co", "json"].join("/"),
appleAuth: (origin: string) =>
[apiHost, "auth", "apple", `gate?origin=${origin}`].join("/"),
googleAuth: (origin: string) =>
[apiHost, "auth", "google", `gate?origin=${origin}`].join("/"),
user: () => [apiHost, prefix, "user.json"].join("/"),
token: () => [apiHost, prefix, "auth", "token.json"].join("/"),
elements: () => [apiHost, prefix, "elements.json"].join("/"),
// token: () => [apiHost, prefix, "auth", "token.json"].join("/"),
elements: () => [oldBackendPrefix, "elements.json"].join("/"),
zodiacs: (zodiac: string) =>
[apiHost, prefix, "zodiacs", `${zodiac}.json`].join("/"),
element: (type: string) =>
[apiHost, prefix, "elements", `${type}.json`].join("/"),
// element: (type: string) =>
// [apiHost, prefix, "elements", `${type}.json`].join("/"),
apps: (bundleId: string) =>
[apiHost, prefix, "apps", `${bundleId}.json`].join("/"),
[oldBackendPrefix, `${bundleId}.json`].join("/"),
assets: (category: string) =>
[apiHost, prefix, "assets", "categories", `${category}.json`].join("/"),
assetCategories: () =>
@ -319,15 +318,13 @@ const routes = {
dailyForecasts: () =>
[apiHost, prefix, "user", "daily_forecast.json"].join("/"),
auras: () => [apiHost, prefix, "user", "aura.json"].join("/"),
paymentIntents: () =>
[apiHost, prefix, "user", "payment_intents.json"].join("/"),
subscriptionItems: () =>
[apiHost, prefix, "user", "subscription", "item_prices.json"].join("/"),
subscriptionPlans: () => [apiHost, prefix, "sub_plans.json"].join("/"),
subscriptionCheckout: () =>
[apiHost, prefix, "user", "subscription", "checkout", "new.json"].join(
"/"
),
// subscriptionItems: () =>
// [apiHost, prefix, "user", "subscription", "item_prices.json"].join("/"),
// subscriptionPlans: () => [apiHost, prefix, "sub_plans.json"].join("/"),
// subscriptionCheckout: () =>
// [apiHost, prefix, "user", "subscription", "checkout", "new.json"].join(
// "/"
// ),
subscriptionStatus: () =>
[apiHost, prefix, "user", "subscription_receipts", "status.json"].join(
"/"
@ -336,10 +333,10 @@ const routes = {
[dApiHost, "users", "subscription", "status"].join("/"),
subscriptionReceipts: () =>
[apiHost, prefix, "user", "subscription_receipts.json"].join("/"),
subscriptionReceipt: (id: string) =>
[apiHost, prefix, "user", "subscription_receipts", `${id}.json`].join(
"/"
),
// subscriptionReceipt: (id: string) =>
// [apiHost, prefix, "user", "subscription_receipts", `${id}.json`].join(
// "/"
// ),
compatCategories: () =>
[apiHost, prefix, "ai", "compat_categories.json"].join("/"),
compat: () => [apiHost, prefix, "ai", "compats.json"].join("/"),
@ -347,7 +344,8 @@ const routes = {
[apiHost, prefix, "user", "callbacks.json"].join("/"),
getUserCallbacks: (id: string) =>
[apiHost, prefix, "user", "callbacks", `${id}.json`].join("/"),
getTranslations: () => [siteHost, "api/v2", "t.json"].join("/"),
getTranslations: () => [oldBackendPrefix, "t.json"].join("/"),
// getTranslations: () => [siteHost, "api/v2", "t.json"].join("/"),
aiRequestsV2: (promptKey: string) =>
[apiHost, "api/v2", "ai", "prompts", promptKey, "requests.json"].join(
"/"
@ -395,6 +393,7 @@ const routes = {
// Session
createSession: () => [dApiHost, dApiPrefix, "session"].join("/"),
updateSession: (id: string) => [dApiHost, dApiPrefix, "session", id].join("/"),
getLocale: () => [dApiHost, dApiPrefix, "session", "locale"].join("/"),
// Chats
getChatsCategories: () => [dApiHost, "chats", "categories"].join("/"),
@ -422,7 +421,7 @@ const routes = {
export const entrypoints = [
routes.client.root(),
routes.client.birthday(),
routes.client.subscription(),
// routes.client.subscription(),
routes.client.wallpaper(),
routes.client.didYouKnow(),
routes.client.attention(),
@ -451,7 +450,7 @@ export const hasNoNavigation = (path: string) => !hasNavigation(path);
export const withCrossButtonRoutes = [
// routes.client.attention(),
routes.client.subscription(),
// routes.client.subscription(),
routes.client.paymentMethod(),
];
/**
@ -562,7 +561,7 @@ export const withoutHeaderRoutes = [
routes.client.palmistryDiscount(),
routes.client.palmistryPremiumBundle(),
routes.client.compatibility(),
routes.client.subscription(),
// routes.client.subscription(),
routes.client.paymentMethod(),
routes.client.paymentResult(),
routes.client.paymentSuccess(),

View File

@ -1,4 +1,9 @@
import { ITrial } from "@/api/resources/SubscriptionPlans";
interface ITrial {
is_paid: boolean;
is_free: boolean;
days: number;
price_cents: number;
}
export const roundToWhole = (value: string | number): number => {
value = Number(value);

View File

@ -39,14 +39,7 @@ import onboardingConfig, {
import payment, {
actions as paymentActions,
selectActiveProduct,
selectIsDiscount,
selectStripeButton,
selectSubscriptionReceipt,
} from "./payment";
import subscriptionPlans, {
actions as subscriptionPlasActions,
selectPlanById,
} from "./subscriptionPlan";
import status, { actions as userStatusActions, selectStatus } from "./status";
import compatibility, {
actions as compatibilityActions,
@ -100,7 +93,6 @@ export const actions = {
user: userActions,
form: formActions,
status: userStatusActions,
subscriptionPlan: subscriptionPlasActions,
aura: auraActions,
paywalls: paywallsActions,
siteConfig: siteConfigActions,
@ -126,7 +118,6 @@ export const selectors = {
selectUTM,
selectUser,
selectStatus,
selectPlanById,
selectAuraCoordinates,
selectRightUser,
selectSelfName,
@ -136,8 +127,6 @@ export const selectors = {
selectUserCallbacksDescription,
selectUserCallbacksPrevStat,
selectHome,
selectIsDiscount,
selectSubscriptionReceipt,
selectOnboarding,
selectOnboardingHome,
selectOnboardingCompatibility,
@ -160,7 +149,6 @@ export const selectors = {
selectPaywalls,
selectPaywallsIsMustUpdate,
selectPrivacyPolicy,
selectStripeButton,
selectPersonalVideo,
selectCurrency,
selectPalmistryFromRedesign,
@ -183,7 +171,6 @@ export const reducer = combineReducers({
user,
form,
status,
subscriptionPlans,
aura,
payment,
compatibility,

View File

@ -1,5 +1,4 @@
import { IPaywallProduct } from "@/api/resources/Paywall";
import { SubscriptionReceipt } from "@/api/resources/UserSubscriptionReceipts";
import { TCanMakePaymentResult } from "@/hooks/payment/useCanUseStripeButton";
import { createSlice, createSelector } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
@ -12,21 +11,12 @@ interface IStripeButton {
interface IPayment {
selectedPrice: number | null;
isDiscount: boolean;
subscriptionReceipt: SubscriptionReceipt | null;
activeProduct: IPaywallProduct | null;
stripeButton: IStripeButton;
}
const initialState: IPayment = {
selectedPrice: null,
isDiscount: false,
subscriptionReceipt: null,
activeProduct: null,
stripeButton: {
paymentRequest: null,
availableMethods: null,
}
};
const paymentSlice = createSlice({
@ -52,16 +42,4 @@ export const selectActiveProduct = createSelector(
(state: { payment: IPayment }) => state.payment.activeProduct,
(payment) => payment
);
export const selectIsDiscount = createSelector(
(state: { payment: IPayment }) => state.payment.isDiscount,
(payment) => payment
);
export const selectSubscriptionReceipt = createSelector(
(state: { payment: IPayment }) => state.payment.subscriptionReceipt,
(payment) => payment
);
export const selectStripeButton = createSelector(
(state: { payment: IPayment }) => state.payment.stripeButton,
(payment) => payment
);
export default paymentSlice.reducer;

View File

@ -25,6 +25,8 @@ const initialState: TPaywalls = {
"aura.placement.palmistry.main": null,
"aura.placement.palmistry.redesign": null,
"aura.placement.chat": null,
"aura.placement.email.palmistry": null,
"aura.placement.email.palmistry.discount": null,
isMustUpdate: {
"aura.placement.v1.mike": true,
"aura.placement.main": true,
@ -34,6 +36,8 @@ const initialState: TPaywalls = {
"aura.placement.palmistry.main": true,
"aura.placement.palmistry.redesign": true,
"aura.placement.chat": true,
"aura.placement.email.palmistry": true,
"aura.placement.email.palmistry.discount": true,
},
}

View File

@ -1,30 +0,0 @@
import {
createSlice, createEntityAdapter, createSelector, EntityState
} from '@reduxjs/toolkit'
import { SubscriptionItems } from '../api'
type SubscriptionPlan = SubscriptionItems.ItemPrice
const subscriptionPlanAdapter = createEntityAdapter<SubscriptionPlan>({
selectId: (plan) => plan.id,
sortComparer: (a, b) => a.created_at - b.created_at,
})
const initialState = subscriptionPlanAdapter.getInitialState()
const subscriptionPlanSlice = createSlice({
name: 'subscriptionPlans',
initialState,
reducers: {
setAll: subscriptionPlanAdapter.setAll,
},
extraReducers: (builder) => builder.addCase('reset', () => initialState)
})
export const { actions } = subscriptionPlanSlice
const { selectById } = subscriptionPlanAdapter.getSelectors()
export const selectPlanById = (id: string) => createSelector(
(state: { subscriptionPlans: EntityState<SubscriptionPlan> }) => state.subscriptionPlans,
(state) => selectById(state, id)
)
export default subscriptionPlanSlice.reducer