Merge branch 'develop' into 'main'
develop See merge request witapp/aura-webapp!532
This commit is contained in:
commit
4b3626de00
@ -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, it’s your turn. <br><br> Your life-changing astrological forecast is almost ready! Before we share this powerful insight with you, we’re 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 you’ll 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 who’s 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",
|
||||
|
||||
1
public/old-backend/auraweb.json
Normal file
1
public/old-backend/auraweb.json
Normal 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"}]}}
|
||||
1
public/old-backend/elements.json
Normal file
1
public/old-backend/elements.json
Normal file
@ -0,0 +1 @@
|
||||
{"data":{"variant":"a","groups":[]}}
|
||||
1
public/old-backend/t.json
Normal file
1
public/old-backend/t.json
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
25
public/v1/email-marketing/messages.svg
Normal file
25
public/v1/email-marketing/messages.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 72 KiB |
BIN
public/v1/email-marketing/payments.png
Normal file
BIN
public/v1/email-marketing/payments.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 572 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 135 KiB |
706
public/v1/email-marketing/smartphone.svg
Normal file
706
public/v1/email-marketing/smartphone.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 4.5 MiB |
@ -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),
|
||||
|
||||
@ -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" });
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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() })
|
||||
}
|
||||
@ -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",
|
||||
});
|
||||
};
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
});
|
||||
};
|
||||
@ -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" });
|
||||
};
|
||||
@ -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) })
|
||||
}
|
||||
@ -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) })
|
||||
}
|
||||
@ -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 };
|
||||
@ -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";
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -86,4 +86,9 @@
|
||||
text-align: center;
|
||||
color: #121620;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
overflow-x: hidden;
|
||||
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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"]}>
|
||||
|
||||
@ -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;
|
||||
|
||||
134
src/components/Payment/nmi/CheckoutForm/index.tsx
Normal file
134
src/components/Payment/nmi/CheckoutForm/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
134
src/components/Payment/nmi/CheckoutForm/styles.module.scss
Normal file
134
src/components/Payment/nmi/CheckoutForm/styles.module.scss
Normal 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;
|
||||
}
|
||||
97
src/components/Payment/nmi/PaymentForm/index.tsx
Normal file
97
src/components/Payment/nmi/PaymentForm/index.tsx
Normal 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
|
||||
55
src/components/Payment/nmi/PaymentForm/styles.module.scss
Normal file
55
src/components/Payment/nmi/PaymentForm/styles.module.scss
Normal 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;
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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%;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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 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, it’s your turn.
|
||||
<br />
|
||||
<br />
|
||||
Your life-changing astrological forecast is almost ready! Before we share
|
||||
this powerful insight with you, we’re 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 you’ll 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 who’s 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", {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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']}>
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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
|
||||
])
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -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];
|
||||
|
||||
|
||||
18
src/init.tsx
18
src/init.tsx
@ -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;
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
Loading…
Reference in New Issue
Block a user