feat: add authorization with apple and google, add payment systems, minor edits, fixed bugs
This commit is contained in:
parent
5b856aea74
commit
8db05c3fd4
BIN
public/apple-auth-icon.png
Normal file
BIN
public/apple-auth-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 412 B |
3
public/apple-auth-icon.svg
Normal file
3
public/apple-auth-icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.6063 4.78931C20.6999 3.52182 21.4371 1.75647 21.2354 0C19.6594 0.06 17.7528 1.00672 16.6228 2.27271C15.6081 3.39619 14.7223 5.19135 14.9604 6.91333C16.7184 7.04382 18.5127 6.05828 19.6063 4.78931ZM23.5485 15.9375C23.5925 20.4779 27.7045 21.9883 27.75 22.0078C27.7166 22.1143 27.0932 24.1599 25.584 26.2749C24.2781 28.1019 22.9236 29.9214 20.7894 29.9604C18.6932 29.9979 18.0182 28.7695 15.6202 28.7695C13.2237 28.7695 12.4744 29.9213 10.4904 29.9978C8.43059 30.0713 6.86071 28.0212 5.54565 26.2002C2.85485 22.4757 0.799598 15.6751 3.56016 11.0852C4.93135 8.80674 7.38097 7.36168 10.0414 7.32568C12.0633 7.28818 13.973 8.62939 15.2091 8.62939C16.4453 8.62939 18.766 7.01691 21.205 7.25391C22.2258 7.29441 25.0926 7.64828 26.9324 10.2297C26.7838 10.3182 23.5121 12.1426 23.5485 15.9375Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 957 B |
BIN
public/google-auth-icon.png
Normal file
BIN
public/google-auth-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1016 B |
13
public/google-auth-icon.svg
Normal file
13
public/google-auth-icon.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_217_23)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.27907 15C6.27907 14.0257 6.44442 13.0916 6.73953 12.2155L1.57395 8.355C0.567209 10.3555 0 12.6095 0 15C0 17.3884 0.566512 19.6411 1.57186 21.6402L6.73465 17.7723C6.44233 16.9002 6.27907 15.9695 6.27907 15Z" fill="#FBBC05"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3489 6.13636C17.5116 6.13636 19.4651 6.88636 21 8.11364L25.4651 3.75C22.7442 1.43182 19.2558 0 15.3489 0C9.28328 0 4.07025 3.39477 1.57397 8.355L6.73956 12.2155C7.92979 8.67955 11.3226 6.13636 15.3489 6.13636Z" fill="#EB4335"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3489 23.8636C11.3226 23.8636 7.92979 21.3204 6.73956 17.7845L1.57397 21.6443C4.07025 26.6052 9.28328 30 15.3489 30C19.0926 30 22.6668 28.6991 25.3493 26.2616L20.4461 22.5518C19.0626 23.4048 17.3205 23.8636 15.3489 23.8636Z" fill="#34A853"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30 15C30 14.1136 29.8605 13.1591 29.6512 12.2727H15.3489V18.0682H23.5814C23.1698 20.0441 22.0493 21.5632 20.4461 22.5518L25.3493 26.2616C28.1673 23.702 30 19.8893 30 15Z" fill="#4285F4"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_217_23">
|
||||
<rect width="30" height="30" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/padlock_icon_animation_closing.png
Normal file
BIN
public/padlock_icon_animation_closing.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
@ -19,11 +19,16 @@ import {
|
||||
AIRequests,
|
||||
UserCallbacks,
|
||||
Translations,
|
||||
Zodiacs
|
||||
Zodiacs,
|
||||
GoogleAuth,
|
||||
SubscriptionPlans,
|
||||
AppleAuth
|
||||
} 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),
|
||||
getAppConfig: createMethod<Apps.Payload, Apps.Response>(Apps.createRequest),
|
||||
getElement: createMethod<Element.Payload, Element.Response>(Element.createRequest),
|
||||
getElements: createMethod<Elements.Payload, Elements.Response>(Elements.createRequest),
|
||||
@ -34,6 +39,7 @@ const api = {
|
||||
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),
|
||||
getSubscriptionStatus: createMethod<SubscriptionStatus.Payload, SubscriptionStatus.Response>(SubscriptionStatus.createRequest),
|
||||
getSubscriptionReceipt: createMethod<SubscriptionReceipts.GetPayload, SubscriptionReceipts.Response>(SubscriptionReceipts.createGetRequest),
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { apiHost } from "@/routes";
|
||||
import { getAuthHeaders } from "../utils";
|
||||
|
||||
export interface Payload {
|
||||
@ -40,6 +41,6 @@ export interface IAiInputs {
|
||||
}
|
||||
|
||||
export const createRequest = ({ body_check_path, token }: Payload): Request => {
|
||||
const url = new URL(`https://aura.wit.life${body_check_path}`);
|
||||
const url = new URL(`${apiHost}${body_check_path}`);
|
||||
return new Request(url, { method: "GET", headers: getAuthHeaders(token) });
|
||||
};
|
||||
|
||||
13
src/api/resources/AppleAuth.ts
Normal file
13
src/api/resources/AppleAuth.ts
Normal file
@ -0,0 +1,13 @@
|
||||
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,6 +1,4 @@
|
||||
import routes from "@/routes"
|
||||
// import { AuthPayload } from "../types"
|
||||
// import { getAuthHeaders } from "../utils"
|
||||
|
||||
export interface Payload {
|
||||
locale: string
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
import routes from "@/routes"
|
||||
import { AssetCategory } from "./AssetCategories"
|
||||
// import { AuthPayload } from "../types"
|
||||
// import { getAuthHeaders } from "../utils"
|
||||
|
||||
// export interface Payload extends AuthPayload {
|
||||
export interface Payload {
|
||||
category: string
|
||||
page?: number
|
||||
|
||||
14
src/api/resources/GoogleAuth.ts
Normal file
14
src/api/resources/GoogleAuth.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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",
|
||||
});
|
||||
};
|
||||
43
src/api/resources/SubscriptionPlans.ts
Normal file
43
src/api/resources/SubscriptionPlans.ts
Normal file
@ -0,0 +1,43 @@
|
||||
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,121 +1,132 @@
|
||||
import routes from "@/routes"
|
||||
import { AuthPayload } from "../types"
|
||||
import { getAuthHeaders } from "../utils"
|
||||
import routes from "@/routes";
|
||||
import { AuthPayload } from "../types";
|
||||
import { getAuthHeaders } from "../utils";
|
||||
|
||||
export type GetPayload = AuthPayload
|
||||
export type GetPayload = AuthPayload;
|
||||
|
||||
export interface PatchPayload extends AuthPayload {
|
||||
user: Partial<UserPatch>
|
||||
user: Partial<UserPatch>;
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
user: User
|
||||
user: User;
|
||||
meta?: {
|
||||
links: {
|
||||
self: string
|
||||
}
|
||||
}
|
||||
self: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserPatch {
|
||||
locale: string
|
||||
timezone: string
|
||||
profile_attributes: Partial<Pick<UserProfile, 'gender' | 'full_name' | 'relationship_status' | 'birthday'> & {
|
||||
birthplace_id: null
|
||||
birthplace_attributes: {
|
||||
address?: string
|
||||
coords?: string
|
||||
},
|
||||
remote_userpic_url: string
|
||||
}>
|
||||
daily_push_subs_attributes: [{
|
||||
time: string
|
||||
daily_push_id: string
|
||||
}]
|
||||
locale: string;
|
||||
timezone: string;
|
||||
profile_attributes: Partial<
|
||||
Pick<
|
||||
UserProfile,
|
||||
"gender" | "full_name" | "relationship_status" | "birthday"
|
||||
> & {
|
||||
birthplace_id: null;
|
||||
birthplace_attributes: {
|
||||
address?: string;
|
||||
coords?: string;
|
||||
};
|
||||
remote_userpic_url: string;
|
||||
}
|
||||
>;
|
||||
daily_push_subs_attributes: [
|
||||
{
|
||||
time: string;
|
||||
daily_push_id: string;
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string | null | undefined
|
||||
username: string | null
|
||||
email: string
|
||||
locale: string
|
||||
state: string
|
||||
timezone: string
|
||||
new_registration: boolean
|
||||
id: string | null | undefined;
|
||||
username: string | null;
|
||||
email: string;
|
||||
locale: string;
|
||||
state: string;
|
||||
timezone: string;
|
||||
new_registration: boolean;
|
||||
stat: {
|
||||
last_online_at: string | null
|
||||
prev_online_at: string | null
|
||||
}
|
||||
profile: UserProfile
|
||||
daily_push_subs: Subscription[]
|
||||
last_online_at: string | null;
|
||||
prev_online_at: string | null;
|
||||
};
|
||||
profile: UserProfile;
|
||||
daily_push_subs: Subscription[];
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
full_name: string | null
|
||||
gender: string | null
|
||||
birthday: string | null
|
||||
birthplace: UserBirhplace | null
|
||||
age: UserAge | null
|
||||
sign: UserSign | null
|
||||
userpic: UserPic | null
|
||||
userpic_mime_type: string | undefined
|
||||
relationship_status: string
|
||||
human_relationship_status: string
|
||||
full_name: string | null;
|
||||
gender: string | null;
|
||||
birthday: string | null;
|
||||
birthplace: UserBirhplace | null;
|
||||
age: UserAge | null;
|
||||
sign: UserSign | null;
|
||||
userpic: UserPic | null;
|
||||
userpic_mime_type: string | undefined;
|
||||
relationship_status: string;
|
||||
human_relationship_status: string;
|
||||
}
|
||||
|
||||
export interface UserAge {
|
||||
years: number
|
||||
days: number
|
||||
years: number;
|
||||
days: number;
|
||||
}
|
||||
|
||||
export interface UserSign {
|
||||
house: number
|
||||
ruler: string
|
||||
house: number;
|
||||
ruler: string;
|
||||
dates: {
|
||||
start: {
|
||||
month: number
|
||||
day: number
|
||||
}
|
||||
month: number;
|
||||
day: number;
|
||||
};
|
||||
end: {
|
||||
month: number
|
||||
day: number
|
||||
}
|
||||
}
|
||||
sign: string
|
||||
char: string
|
||||
polarity: string
|
||||
modality: string
|
||||
triplicity: string
|
||||
month: number;
|
||||
day: number;
|
||||
};
|
||||
};
|
||||
sign: string;
|
||||
char: string;
|
||||
polarity: string;
|
||||
modality: string;
|
||||
triplicity: string;
|
||||
}
|
||||
|
||||
export interface UserPic {
|
||||
th: string
|
||||
th2x: string
|
||||
lg: string
|
||||
th: string;
|
||||
th2x: string;
|
||||
lg: string;
|
||||
}
|
||||
|
||||
export interface UserBirhplace {
|
||||
id: string
|
||||
address: string
|
||||
coords: string
|
||||
id: string;
|
||||
address: string;
|
||||
coords: string;
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
id: string
|
||||
daily_push_id: string
|
||||
time: string
|
||||
updated_at: string
|
||||
created_at: string
|
||||
last_sent_at: string | null
|
||||
id: string;
|
||||
daily_push_id: string;
|
||||
time: string;
|
||||
updated_at: string;
|
||||
created_at: string;
|
||||
last_sent_at: string | null;
|
||||
}
|
||||
|
||||
export const createGetRequest = ({ token }: GetPayload): Request => {
|
||||
const url = new URL(routes.server.user())
|
||||
return new Request(url, { method: 'GET', headers: getAuthHeaders(token) })
|
||||
}
|
||||
const url = new URL(routes.server.user());
|
||||
return new Request(url, { method: "GET", headers: getAuthHeaders(token) });
|
||||
};
|
||||
|
||||
export const createPatchRequest = ({ token, user }: PatchPayload): Request => {
|
||||
const url = new URL(routes.server.user())
|
||||
const body = JSON.stringify({ user })
|
||||
return new Request(url, { method: 'PATCH', headers: getAuthHeaders(token), body })
|
||||
}
|
||||
const url = new URL(routes.server.user());
|
||||
const body = JSON.stringify({ user });
|
||||
return new Request(url, {
|
||||
method: "PATCH",
|
||||
headers: getAuthHeaders(token),
|
||||
body,
|
||||
});
|
||||
};
|
||||
|
||||
@ -19,13 +19,22 @@ export interface AppleReceiptPayload extends AuthPayload {
|
||||
}
|
||||
|
||||
export interface StripeReceiptPayload extends AuthPayload {
|
||||
way: "stripe";
|
||||
subscription_receipt: {
|
||||
sub_plan_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PayPalReceiptPayload extends AuthPayload {
|
||||
itemInterval: "week" | "month" | "year";
|
||||
way: "paypal";
|
||||
}
|
||||
|
||||
export type Payload =
|
||||
| ChargebeeReceiptPayload
|
||||
| AppleReceiptPayload
|
||||
| StripeReceiptPayload;
|
||||
| StripeReceiptPayload
|
||||
| PayPalReceiptPayload;
|
||||
|
||||
export interface Response {
|
||||
subscription_receipt: SubscriptionReceipt;
|
||||
@ -54,9 +63,16 @@ export interface SubscriptionReceipt {
|
||||
app_bundle_id: string;
|
||||
autorenewable: boolean;
|
||||
error: string;
|
||||
links?: IPayPalLink[];
|
||||
};
|
||||
}
|
||||
|
||||
interface IPayPalLink {
|
||||
href: string;
|
||||
rel: "approve" | "edit" | "self";
|
||||
method: "GET" | "PATCH";
|
||||
}
|
||||
|
||||
function createRequest({
|
||||
token,
|
||||
itemPriceId,
|
||||
@ -69,7 +85,7 @@ function createRequest({
|
||||
autorenewable = true,
|
||||
sandbox = true,
|
||||
}: AppleReceiptPayload): Request;
|
||||
function createRequest({ token, itemInterval }: StripeReceiptPayload): Request;
|
||||
function createRequest({ token }: StripeReceiptPayload): Request;
|
||||
function createRequest(payload: Payload): Request;
|
||||
function createRequest(payload: Payload): Request {
|
||||
const url = new URL(routes.server.subscriptionReceipts());
|
||||
@ -103,11 +119,19 @@ function getDataPayload(payload: Payload) {
|
||||
},
|
||||
};
|
||||
}
|
||||
if ("itemInterval" in payload) {
|
||||
if ("way" in payload && payload.way === "paypal") {
|
||||
return {
|
||||
way: "paypal",
|
||||
subscription_receipt: {
|
||||
item_interval: payload.itemInterval,
|
||||
},
|
||||
};
|
||||
}
|
||||
if ("way" in payload && payload.way === "stripe") {
|
||||
return {
|
||||
way: "stripe",
|
||||
subscription_receipt: {
|
||||
item_interval: payload.itemInterval,
|
||||
sub_plan_id: payload.subscription_receipt.sub_plan_id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
export * as Assets from './Assets'
|
||||
export * as AssetCategories from './AssetCategories'
|
||||
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 SubscriptionReceipts from './UserSubscriptionReceipts'
|
||||
export * as PaymentIntents from './UserPaymentIntents'
|
||||
export * as AICompatCategories from './AICompatCategories'
|
||||
export * as AICompats from './AICompats'
|
||||
export * as AIRequests from './AIRequests'
|
||||
export * as UserCallbacks from './UserCallbacks'
|
||||
export * as Translations from './Translations'
|
||||
export * as Zodiacs from './Zodiacs'
|
||||
export * as Assets from "./Assets";
|
||||
export * as AssetCategories from "./AssetCategories";
|
||||
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 SubscriptionReceipts from "./UserSubscriptionReceipts";
|
||||
export * as PaymentIntents from "./UserPaymentIntents";
|
||||
export * as AICompatCategories from "./AICompatCategories";
|
||||
export * as AICompats from "./AICompats";
|
||||
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";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useContext } from 'react'
|
||||
import { AuthContext } from './AuthContext'
|
||||
import { useContext } from "react";
|
||||
import { AuthContext } from "./AuthContext";
|
||||
|
||||
export const useAuth = () => useContext(AuthContext)
|
||||
export const useAuth = () => useContext(AuthContext);
|
||||
|
||||
@ -48,17 +48,22 @@ import { Asset } from "@/api/resources/Assets";
|
||||
import PaymentResultPage from "../PaymentPage/results";
|
||||
import PaymentSuccessPage from "../PaymentPage/results/SuccessPage";
|
||||
import PaymentFailPage from "../PaymentPage/results/ErrorPage";
|
||||
import { StripePage } from "../StripePage";
|
||||
import AuthPage from "../AuthPage";
|
||||
import AuthResultPage from "../AuthResultPage";
|
||||
|
||||
function App(): JSX.Element {
|
||||
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 } = useAuth();
|
||||
|
||||
const closeSpecialOfferAttention = () => {
|
||||
setIsSpecialOfferOpen(false);
|
||||
navigate(routes.client.emailEnter());
|
||||
navigate(routes.client.auth());
|
||||
};
|
||||
|
||||
const assetsData = useCallback(async () => {
|
||||
@ -71,13 +76,25 @@ function App(): JSX.Element {
|
||||
const { data } = useApiCall<Asset[]>(assetsData);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: remove later
|
||||
dispatch(
|
||||
actions.token.update(
|
||||
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjQwNjEyLCJpYXQiOjE2OTc5MjY0MTksImV4cCI6MTcwNjU2NjQxOSwianRpIjoiZTg0NWE0ZmUtYmVmNy00ODNmLWIwMzgtYjlkYzBlZjk1MjNmIiwiZW1haWwiOiJvdGhlcjJAZXhhbXBsZS5jb20iLCJzdGF0ZSI6InByb3ZlbiIsImxvYyI6ImVuIiwidHoiOjAsInR5cGUiOiJlbWFpbCIsImlzcyI6ImNvbS5saWZlLmF1cmEifQ.ijaHDiNRLUIKdkziVB-zt8DA8WNH7RNwvYkp2EGDxTM"
|
||||
)
|
||||
);
|
||||
}, [dispatch]);
|
||||
(async () => {
|
||||
if (!token.length || !user) return;
|
||||
const {
|
||||
user: { has_subscription },
|
||||
} = await api.getSubscriptionStatus({
|
||||
token,
|
||||
});
|
||||
|
||||
if (has_subscription && user) {
|
||||
return dispatch(actions.status.update("subscribed"));
|
||||
}
|
||||
if (!has_subscription && user) {
|
||||
return dispatch(actions.status.update("unsubscribed"));
|
||||
}
|
||||
if (!user) {
|
||||
return dispatch(actions.status.update("lead"));
|
||||
}
|
||||
})();
|
||||
}, [dispatch, api, token, user]);
|
||||
|
||||
useEffect(() => {
|
||||
async function getApng() {
|
||||
@ -91,61 +108,115 @@ function App(): JSX.Element {
|
||||
getApng();
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const response = await fetch("/padlock_icon_animation_closing.png");
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
setPadLockApng(parseAPNG(arrayBuffer));
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
dispatch(actions.form.addEmail(user.email));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route element={<Layout setIsSpecialOfferOpen={setIsSpecialOfferOpen} />}>
|
||||
<Route path={routes.client.root()} element={<MainPage />} />
|
||||
<Route path={routes.client.birthday()} element={<BirthdayPage />} />
|
||||
<Route path={routes.client.didYouKnow()} element={<DidYouKnowPage />} />
|
||||
<Route
|
||||
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()} element={<BirthtimePage />} />
|
||||
<Route path={routes.client.createProfile()} element={<SkipStep />} />
|
||||
<Route path={routes.client.emailEnter()} element={<EmailEnterPage />} />
|
||||
<Route path={routes.client.static()} element={<StaticPage />} />
|
||||
<Route
|
||||
path={routes.client.compatibility()}
|
||||
element={<CompatibilityPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.compatibilityResult()}
|
||||
element={<CompatResultPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.breath()}
|
||||
element={<BreathPage leoApng={leoApng} />}
|
||||
/>
|
||||
<Route path={routes.client.priceList()} element={<PriceListPage />} />
|
||||
<Route path={routes.client.home()} element={<HomePage />} />
|
||||
<Route
|
||||
path={routes.client.breathResult()}
|
||||
element={<UserCallbacksPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.subscription()}
|
||||
element={<SubscriptionPage />}
|
||||
/>
|
||||
<Route path={routes.client.paymentMethod()} element={<PaymentPage />} />
|
||||
<Route path={routes.client.paymentResult()} element={<PaymentResultPage />} />
|
||||
<Route path={routes.client.paymentSuccess()} element={<PaymentSuccessPage />} />
|
||||
<Route path={routes.client.paymentFail()} element={<PaymentFailPage />} />
|
||||
<Route
|
||||
<Route element={<AuthorizedUserOutlet />}>
|
||||
<Route path={routes.client.root()} element={<MainPage />} />
|
||||
<Route path={routes.client.birthday()} element={<BirthdayPage />} />
|
||||
<Route
|
||||
path={routes.client.didYouKnow()}
|
||||
element={<DidYouKnowPage />}
|
||||
/>
|
||||
<Route
|
||||
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()} element={<BirthtimePage />} />
|
||||
<Route path={routes.client.createProfile()} element={<SkipStep />} />
|
||||
<Route
|
||||
path={routes.client.emailEnter()}
|
||||
element={<EmailEnterPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.auth()}
|
||||
element={<AuthPage padLockApng={padLockApng} />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.authResult()}
|
||||
element={<AuthResultPage />}
|
||||
/>
|
||||
<Route path={routes.client.static()} element={<StaticPage />} />
|
||||
<Route path={routes.client.priceList()} element={<PriceListPage />} />
|
||||
{/* <Route
|
||||
path={routes.client.wallpaper()}
|
||||
element={<ProtectWallpaperPage />}
|
||||
/>
|
||||
<Route element={<PrivateOutlet />}></Route>
|
||||
/> */}
|
||||
</Route>
|
||||
<Route
|
||||
path={routes.client.subscription()}
|
||||
element={<SubscriptionPage />}
|
||||
/>
|
||||
<Route element={<PrivateOutlet />}>
|
||||
<Route element={<AuthorizedUserOutlet />}>
|
||||
<Route
|
||||
path={routes.client.paymentMethod()}
|
||||
element={<PaymentPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.paymentResult()}
|
||||
element={<PaymentResultPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.paymentSuccess()}
|
||||
element={<PaymentSuccessPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.paymentFail()}
|
||||
element={<PaymentFailPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.paymentStripe()}
|
||||
element={<StripePage />}
|
||||
/>
|
||||
</Route>
|
||||
<Route element={<PrivateSubscriptionOutlet />}>
|
||||
<Route path={routes.client.home()} element={<HomePage />} />
|
||||
<Route
|
||||
path={routes.client.compatibility()}
|
||||
element={<CompatibilityPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.compatibilityResult()}
|
||||
element={<CompatResultPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.breath()}
|
||||
element={<BreathPage leoApng={leoApng} />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.breathResult()}
|
||||
element={<UserCallbacksPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.wallpaper()}
|
||||
element={<WallpaperPage />}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
@ -248,6 +319,16 @@ function Layout({ setIsSpecialOfferOpen }: LayoutProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function AuthorizedUserOutlet(): JSX.Element {
|
||||
const status = useSelector(selectors.selectStatus);
|
||||
const { user } = useAuth();
|
||||
return user && status === "subscribed" ? (
|
||||
<Navigate to={routes.client.home()} replace={true} />
|
||||
) : (
|
||||
<Outlet />
|
||||
);
|
||||
}
|
||||
|
||||
function PrivateOutlet(): JSX.Element {
|
||||
const { user } = useAuth();
|
||||
return user ? (
|
||||
@ -257,6 +338,15 @@ function PrivateOutlet(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function PrivateSubscriptionOutlet(): JSX.Element {
|
||||
const status = useSelector(selectors.selectStatus);
|
||||
return status === "subscribed" ? (
|
||||
<Outlet />
|
||||
) : (
|
||||
<Navigate to={getRouteBy(status)} replace={true} />
|
||||
);
|
||||
}
|
||||
|
||||
function SkipStep(): JSX.Element {
|
||||
const { user } = useAuth();
|
||||
return user ? (
|
||||
@ -271,14 +361,4 @@ function MainPage(): JSX.Element {
|
||||
return <Navigate to={getRouteBy(status)} replace={true} />;
|
||||
}
|
||||
|
||||
function ProtectWallpaperPage(): JSX.Element {
|
||||
const status = useSelector(selectors.selectStatus);
|
||||
return <WallpaperPage />;
|
||||
return status === "subscribed" ? (
|
||||
<WallpaperPage />
|
||||
) : (
|
||||
<Navigate to={getRouteBy(status)} replace={true} />
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@ -1,43 +1,40 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Title from '../Title'
|
||||
import routes from '@/routes'
|
||||
import styles from './styles.module.css'
|
||||
// import CheckboxWithText from '../CheckboxWithText'
|
||||
import SpecialWelcomeOffer from '../SpecialWelcomeOffer'
|
||||
import MainButton from '../MainButton'
|
||||
// import MainButton from '../MainButton'
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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
|
||||
isOpenModal: boolean;
|
||||
onCloseSpecialOffer?: () => void;
|
||||
}
|
||||
|
||||
function AttentionPage({ isOpenModal, onCloseSpecialOffer }: AttentionPageProps): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const handleNext = () => navigate(routes.client.feedback())
|
||||
|
||||
// const onChangeCheckbox = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
// if (e.currentTarget.checked) {
|
||||
// handleNext()
|
||||
// }
|
||||
// }
|
||||
function AttentionPage({
|
||||
isOpenModal,
|
||||
onCloseSpecialOffer,
|
||||
}: AttentionPageProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const handleNext = () => navigate(routes.client.feedback());
|
||||
|
||||
return (
|
||||
<section className={`${styles.page} page`}>
|
||||
<SpecialWelcomeOffer open={isOpenModal} onClose={onCloseSpecialOffer} />
|
||||
<img className={styles.icon} src="/stop-icon.png" alt="stop" />
|
||||
<Title variant='h2'>{t('aura.attention.title')}</Title>
|
||||
<p className={styles.text}>{t('aura.warming_up.body')}</p>
|
||||
<div className={styles['buttons-container']}>
|
||||
{/* <CheckboxWithText text={t('not_ready_for_information')} onChange={onChangeCheckbox} /> */}
|
||||
{/* <Title variant='h2' className={styles.button} onClick={handleNext}>{t('aura.warming_up.button')}</Title> */}
|
||||
<MainButton onClick={handleNext}>{t('aura.warmin_good.button')}</MainButton>
|
||||
<MainButton onClick={handleNext}>{t('aura.warmin_bad.button')}</MainButton>
|
||||
<Title variant="h2">{t("aura.attention.title")}</Title>
|
||||
<p className={styles.text}>{t("aura.warming_up.body")}</p>
|
||||
<div className={styles["buttons-container"]}>
|
||||
<MainButton onClick={handleNext}>
|
||||
{t("aura.warmin_good.button")}
|
||||
</MainButton>
|
||||
<MainButton onClick={handleNext}>
|
||||
{t("aura.warmin_bad.button")}
|
||||
</MainButton>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default AttentionPage
|
||||
export default AttentionPage;
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
margin: 64px auto 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 13px;
|
||||
}
|
||||
|
||||
|
||||
17
src/components/AuthPage/AppleAuthButton/index.tsx
Normal file
17
src/components/AuthPage/AppleAuthButton/index.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
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.svg" alt="Apple" />
|
||||
{"Sign in with Apple"}
|
||||
</MainButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppleAuthButton;
|
||||
11
src/components/AuthPage/AppleAuthButton/styles.module.css
Normal file
11
src/components/AuthPage/AppleAuthButton/styles.module.css
Normal file
@ -0,0 +1,11 @@
|
||||
.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;
|
||||
}
|
||||
17
src/components/AuthPage/GoogleAuthButton/index.tsx
Normal file
17
src/components/AuthPage/GoogleAuthButton/index.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
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;
|
||||
11
src/components/AuthPage/GoogleAuthButton/styles.module.css
Normal file
11
src/components/AuthPage/GoogleAuthButton/styles.module.css
Normal file
@ -0,0 +1,11 @@
|
||||
.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;
|
||||
}
|
||||
96
src/components/AuthPage/index.tsx
Normal file
96
src/components/AuthPage/index.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import Policy from "../Policy";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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 { t } = useTranslation();
|
||||
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}>{t("we_dont_share")}</p>
|
||||
<div className={styles["buttons-container"]}>
|
||||
<GoogleAuthButton onClick={handleGoogleAuth} />
|
||||
<AppleAuthButton onClick={handleAppleAuth} />
|
||||
</div>
|
||||
<Policy className={styles.policy} sizing="medium">
|
||||
{t("_continue_agree", {
|
||||
eulaLink: (
|
||||
<a
|
||||
href="https://aura.wit.life/terms"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("eula")}
|
||||
</a>
|
||||
),
|
||||
privacyLink: (
|
||||
<a
|
||||
href="https://aura.wit.life/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("privacy_policy")}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</Policy>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthPage;
|
||||
41
src/components/AuthPage/styles.module.css
Normal file
41
src/components/AuthPage/styles.module.css
Normal file
@ -0,0 +1,41 @@
|
||||
.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;
|
||||
}
|
||||
86
src/components/AuthResultPage/index.tsx
Normal file
86
src/components/AuthResultPage/index.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { ApiError, extractErrorMessage, useApi } from "@/api";
|
||||
import Loader from "../Loader";
|
||||
import styles from "./styles.module.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import Title from "../Title";
|
||||
import MainButton from "../MainButton";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import routes from "@/routes";
|
||||
import { useAuth } from "@/auth";
|
||||
import ErrorText from "../ErrorText";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { actions, selectors } from "@/store";
|
||||
|
||||
function AuthResultPage(): JSX.Element {
|
||||
const api = useApi();
|
||||
const { signUp } = useAuth();
|
||||
const dispatch = useDispatch();
|
||||
const birthday = useSelector(selectors.selectBirthday);
|
||||
const [apiError, setApiError] = useState<ApiError | null>(null);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const navigate = useNavigate();
|
||||
const queryParameters = new URLSearchParams(window.location.search);
|
||||
const access_token = queryParameters.get("jwt") || "";
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const apiUser = await api.getUser({ token: access_token });
|
||||
signUp(access_token, apiUser.user);
|
||||
const payload = {
|
||||
user: { profile_attributes: { birthday } },
|
||||
token: access_token,
|
||||
};
|
||||
const updatedUser = await api.updateUser(payload).catch((error) => {
|
||||
console.log("Error: ", error);
|
||||
});
|
||||
if (updatedUser?.user) {
|
||||
dispatch(actions.user.update(updatedUser.user));
|
||||
}
|
||||
dispatch(actions.status.update("registred"));
|
||||
setIsLoading(false);
|
||||
setTimeout(() => {
|
||||
navigate(routes.client.priceList());
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof ApiError) {
|
||||
setApiError(error as ApiError);
|
||||
} else {
|
||||
setError(true);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const handleTryAgain = () => {
|
||||
navigate(routes.client.auth());
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={`${styles.page} page`}>
|
||||
{isLoading && <Loader />}
|
||||
{(error || apiError) && (
|
||||
<>
|
||||
<Title>Something went wrong</Title>
|
||||
<MainButton onClick={handleTryAgain}>Try again</MainButton>
|
||||
</>
|
||||
)}
|
||||
{apiError && (
|
||||
<ErrorText
|
||||
size="medium"
|
||||
isShown={Boolean(apiError)}
|
||||
message={apiError ? extractErrorMessage(apiError) : null}
|
||||
/>
|
||||
)}
|
||||
{!apiError && !error && !isLoading && access_token.length && (
|
||||
<img src="/SuccessIcon.png" alt="Success Icon" />
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthResultPage;
|
||||
10
src/components/AuthResultPage/styles.module.css
Normal file
10
src/components/AuthResultPage/styles.module.css
Normal file
@ -0,0 +1,10 @@
|
||||
.page {
|
||||
position: relative;
|
||||
height: calc(100vh - 103px);
|
||||
max-height: -webkit-fill-available;
|
||||
flex: auto;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
grid-template-rows: 1fr 96px;
|
||||
justify-items: center;
|
||||
}
|
||||
@ -42,8 +42,6 @@ function CompatResultPage(): JSX.Element {
|
||||
return navigate(routes.client.compatibility());
|
||||
};
|
||||
|
||||
// const handleCompatibility = () => navigate(routes.client.compatibility());
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
const right_bday =
|
||||
typeof rightUser.birthDate === "string"
|
||||
|
||||
@ -30,7 +30,7 @@ const DatePicker: React.FC<DatePickerProps> = ({
|
||||
});
|
||||
|
||||
const months = Array.from({ length: 36 }, (_, index) =>
|
||||
new Date(0, index).toLocaleDateString(undefined, { month: "long" })
|
||||
new Date(0, index).toLocaleDateString(locale, { month: "long" })
|
||||
);
|
||||
const years = Array.from({ length: 81 }, (_, index) =>
|
||||
(currentDate.getFullYear() - 80 + index).toString()
|
||||
@ -60,6 +60,7 @@ const DatePicker: React.FC<DatePickerProps> = ({
|
||||
}
|
||||
}, [selectedDate, onDateChange]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles["date-picker-container"]}>
|
||||
|
||||
@ -48,8 +48,10 @@ const DatePickerItem: React.FC<DatePickerItemProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTranslateY((data.indexOf(selectedValue) + (unit === "month" ? 12 : 0)) * -ITEM_HEIGHT)
|
||||
}, [selectedValue, data, unit])
|
||||
setTranslateY(
|
||||
(data.indexOf(selectedValue) + (unit === "month" ? 12 : 0)) * -ITEM_HEIGHT
|
||||
);
|
||||
}, [selectedValue, data, unit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (unit === "month") {
|
||||
@ -98,48 +100,6 @@ const DatePickerItem: React.FC<DatePickerItemProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
// if (!isMobile) {
|
||||
// setTouchY(event.clientY);
|
||||
// document.addEventListener("mousemove", handleMouseMove);
|
||||
// document.addEventListener("mouseup", handleMouseUp);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleMouseMove = (event: MouseEvent) => {
|
||||
// const deltaY = event.clientY - touchY;
|
||||
// handleScroll(deltaY);
|
||||
// setTouchY(event.clientY);
|
||||
// };
|
||||
|
||||
// const handleMouseUp = () => {
|
||||
// resetMouseState();
|
||||
// };
|
||||
|
||||
// const resetTouchState = () => {
|
||||
// if (isMobile && scrollRef.current) {
|
||||
// const selectedIndex = Math.round(-translateY / ITEM_HEIGHT);
|
||||
// onSelect(data[selectedIndex]);
|
||||
|
||||
// // Limit the translateY to ensure it aligns with a valid item
|
||||
// setTranslateY(-selectedIndex * ITEM_HEIGHT);
|
||||
// setTouchY(0);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const resetMouseState = () => {
|
||||
// document.removeEventListener("mousemove", handleMouseMove);
|
||||
// document.removeEventListener("mouseup", handleMouseUp);
|
||||
// resetTouchState();
|
||||
// };
|
||||
|
||||
// useEffect(() => {
|
||||
// // Clean up mouse event listeners when the component unmounts
|
||||
// return () => {
|
||||
// resetMouseState();
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<div className={styles["date-picker-viewport"]}>
|
||||
<div className={styles["date-picker-wheel"]}>
|
||||
|
||||
@ -176,9 +176,6 @@ function CompatibilityPage(): JSX.Element {
|
||||
{!showNavbarFooter && (
|
||||
<div className={styles.cross} onClick={handleCross}></div>
|
||||
)}
|
||||
{/* <Title variant="h1" className={styles.title}>
|
||||
{t("compatibility")}
|
||||
</Title> */}
|
||||
<div className={styles.content}>
|
||||
{onboardingCompatibility && (
|
||||
<Title variant="h2" className={styles.iam}>
|
||||
|
||||
@ -38,9 +38,6 @@ const calculateTop = (currentIdx: number, length: number, items: HTMLDivElement[
|
||||
if (!item) return accumulator;
|
||||
return accumulator + item.clientHeight
|
||||
}, 1) + 8 * getMultiplier(currentIdx, length)
|
||||
|
||||
// const itemHeight = 63
|
||||
// return getMultiplier(currentIdx, length) * itemHeight1?.clientHeight
|
||||
}
|
||||
|
||||
function ProcessFlow({ items, onDone }: ProcessFlowProps): JSX.Element {
|
||||
|
||||
@ -1,44 +1,38 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MainButton from '../MainButton'
|
||||
import Title from '../Title'
|
||||
import routes from '@/routes'
|
||||
import styles from './styles.module.css'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { selectors } from '@/store'
|
||||
import { getZodiacSignByDate } from '@/services/zodiac-sign'
|
||||
// import SpecialWelcomeOffer from '../SpecialWelcomeOffer'
|
||||
// import { useState } from 'react'
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import MainButton from "../MainButton";
|
||||
import Title from "../Title";
|
||||
import routes from "@/routes";
|
||||
import styles from "./styles.module.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectors } from "@/store";
|
||||
import { getZodiacSignByDate } from "@/services/zodiac-sign";
|
||||
|
||||
function DidYouKnowPage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const handleNext = () => navigate(routes.client.freePeriodInfo())
|
||||
// const [isOpenModal, setIsOpenModal] = useState(false)
|
||||
// const handleSpecialOffer = () => {
|
||||
// setIsOpenModal(true)
|
||||
// }
|
||||
const birthdate = useSelector(selectors.selectBirthdate)
|
||||
const zodiacSign = getZodiacSignByDate(birthdate)
|
||||
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const handleNext = () => navigate(routes.client.freePeriodInfo());
|
||||
const birthdate = useSelector(selectors.selectBirthdate);
|
||||
const zodiacSign = getZodiacSignByDate(birthdate);
|
||||
|
||||
return (
|
||||
<section className={`${styles.page} page`}>
|
||||
{/* <SpecialWelcomeOffer open={isOpenModal} /> */}
|
||||
<div className={styles.content}>
|
||||
<Title variant='h1'>{t('did_you_know')}</Title>
|
||||
<Title variant="h1">{t("did_you_know")}</Title>
|
||||
<p className={styles.zodiacInfo}>
|
||||
{t('zodiac_sign_info', { zodiacSign })}
|
||||
{t("zodiac_sign_info", { zodiacSign })}
|
||||
</p>
|
||||
</div>
|
||||
<footer className={styles.footer}>
|
||||
<MainButton onClick={handleNext}>
|
||||
{t('learn_about_my_energy')}
|
||||
{t("learn_about_my_energy")}
|
||||
</MainButton>
|
||||
<span className={styles.skip} onClick={handleNext}>{t('skip_for_now')}</span>
|
||||
<span className={styles.skip} onClick={handleNext}>
|
||||
{t("skip_for_now")}
|
||||
</span>
|
||||
</footer>
|
||||
</section>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default DidYouKnowPage
|
||||
export default DidYouKnowPage;
|
||||
|
||||
@ -12,7 +12,7 @@ function FeedbackPage(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const api = useApi();
|
||||
const handleNext = () => navigate(routes.client.emailEnter());
|
||||
const handleNext = () => navigate(routes.client.auth());
|
||||
|
||||
const assetsData = useCallback(async () => {
|
||||
const { assets } = await api.getAssets({ category: String("au") });
|
||||
|
||||
@ -1,18 +1,10 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { selectors } from "@/store";
|
||||
import { usePayment } from "@/payment";
|
||||
import { actions } from "@/store";
|
||||
import {
|
||||
ApplePayBanner,
|
||||
// ApplePayButton,
|
||||
GooglePayBanner,
|
||||
// GooglePayButton,
|
||||
// CardButton,
|
||||
// CardModal,
|
||||
} from "./methods";
|
||||
import { ApplePayBanner, GooglePayBanner } from "./methods";
|
||||
import ErrorModal from "./ErrorModal";
|
||||
import UserHeader from "../UserHeader";
|
||||
import Title from "../Title";
|
||||
@ -21,33 +13,56 @@ import secure from "./secure.png";
|
||||
import routes from "@/routes";
|
||||
import "./styles.css";
|
||||
import Header from "../Header";
|
||||
import { StripeButton, StripeModal } from "./methods/Stripe";
|
||||
import { StripeButton } from "./methods/Stripe";
|
||||
import { PayPalButton } from "./methods/PayPal/Button";
|
||||
import { useAuth } from "@/auth";
|
||||
import { useApi } from "@/api";
|
||||
import { PayPalReceiptPayload } from "@/api/resources/UserSubscriptionReceipts";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
|
||||
const getPrice = (activeSubPlan: ISubscriptionPlan | null) => {
|
||||
if (!activeSubPlan) return 0;
|
||||
return String(
|
||||
activeSubPlan?.trial?.price_cents
|
||||
? activeSubPlan.trial.price_cents / 100
|
||||
: 0
|
||||
);
|
||||
};
|
||||
|
||||
function PaymentPage(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const { applePay } = usePayment();
|
||||
// const [openCardModal, setOpenCardModal] = useState(false);
|
||||
const [openStripeModal, setOpenStripeModal] = useState(false);
|
||||
const api = useApi();
|
||||
const { token } = useAuth();
|
||||
const [openErrorModal, setOpenErrorModal] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const isLoading = applePay === null;
|
||||
const isApplePayAvailable =
|
||||
import.meta.env.PROD && applePay?.canMakePayments();
|
||||
const email = useSelector(selectors.selectEmail);
|
||||
const isDiscount = useSelector(selectors.selectIsDiscount);
|
||||
const selectedPrice = useSelector(selectors.selectSelectedPrice);
|
||||
const price = isDiscount
|
||||
? (Math.round(selectedPrice || 0) / 2).toFixed(2)
|
||||
: selectedPrice;
|
||||
const onSuccess = useCallback(() => {
|
||||
dispatch(actions.status.update("subscribed"));
|
||||
navigate(routes.client.wallpaper());
|
||||
}, [dispatch, navigate]);
|
||||
const onError = useCallback((error: Error) => {
|
||||
console.error(error);
|
||||
setOpenErrorModal(true);
|
||||
}, []);
|
||||
const activeSubPlan = useSelector(selectors.selectActiveSubPlan);
|
||||
|
||||
const navigateToStripe = () => {
|
||||
navigate(routes.client.paymentStripe());
|
||||
};
|
||||
|
||||
const navigateToPayPal = async () => {
|
||||
const { subscription_receipt } = await api.createSubscriptionReceipt({
|
||||
token,
|
||||
itemInterval: "year",
|
||||
way: "paypal",
|
||||
subscription_receipt: {
|
||||
sub_plan_id: activeSubPlan?.id || "",
|
||||
},
|
||||
} as PayPalReceiptPayload);
|
||||
const url = subscription_receipt.data.links?.find(
|
||||
(link) => link.rel === "approve"
|
||||
)?.href;
|
||||
if (!url?.length) {
|
||||
return setOpenErrorModal(true);
|
||||
}
|
||||
window.location.href = url;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -68,33 +83,21 @@ function PaymentPage(): JSX.Element {
|
||||
<Title variant="h1" className="mb-45">
|
||||
{t("choose_payment")}
|
||||
</Title>
|
||||
{/* {isApplePayAvailable ? (
|
||||
<ApplePayButton onSuccess={onSuccess} onError={onError} />
|
||||
) : (
|
||||
<GooglePayButton onSuccess={onSuccess} onError={onError} />
|
||||
)}
|
||||
<div className="payment-divider">{t("or").toUpperCase()}</div>
|
||||
<CardButton onClick={() => setOpenCardModal(true)} /> */}
|
||||
<StripeButton onClick={() => setOpenStripeModal(true)} />
|
||||
<div className="payment-buttons-container">
|
||||
<PayPalButton onClick={navigateToPayPal} />
|
||||
<StripeButton onClick={navigateToStripe} />
|
||||
</div>
|
||||
<p className="payment-warining">
|
||||
{t("will_be_charged", {
|
||||
strongText: (
|
||||
<strong>{t("trial_price", { price: price })}</strong>
|
||||
<strong>
|
||||
{t("trial_price", {
|
||||
price: getPrice(activeSubPlan || null),
|
||||
})}
|
||||
</strong>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
{/* <CardModal
|
||||
open={openCardModal}
|
||||
onClose={() => setOpenCardModal(false)}
|
||||
onSuccess={onSuccess}
|
||||
onError={onError}
|
||||
/> */}
|
||||
<StripeModal
|
||||
open={openStripeModal}
|
||||
onClose={() => setOpenStripeModal(false)}
|
||||
onSuccess={onSuccess}
|
||||
onError={onError}
|
||||
/>
|
||||
<ErrorModal
|
||||
open={openErrorModal}
|
||||
onClose={() => setOpenErrorModal(false)}
|
||||
|
||||
15
src/components/PaymentPage/methods/PayPal/Button.tsx
Normal file
15
src/components/PaymentPage/methods/PayPal/Button.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import MainButton from "@/components/MainButton";
|
||||
|
||||
interface IPayPalButtonProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function PayPalButton({ onClick }: IPayPalButtonProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<MainButton color="blue" onClick={onClick}>
|
||||
{t("payPal")}
|
||||
</MainButton>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MainButton from '@/components/MainButton'
|
||||
// import card from './card.svg'
|
||||
|
||||
interface IStripeButtonProps {
|
||||
onClick: () => void
|
||||
@ -11,7 +10,6 @@ export function StripeButton({ onClick }: IStripeButtonProps): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<MainButton color='blue' onClick={onClick}>
|
||||
{/* <img className='payment-card' src={card} alt='Credit / Debit Card' /> */}
|
||||
{t('stripe')}
|
||||
</MainButton>
|
||||
)
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
import MainButton from "@/components/MainButton";
|
||||
import Title from "@/components/Title";
|
||||
import { actions } from "@/store";
|
||||
import {
|
||||
PaymentElement,
|
||||
useElements,
|
||||
useStripe,
|
||||
} from "@stripe/react-stripe-js";
|
||||
import { useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
export default function CheckoutForm() {
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [message, setMessage] = useState("");
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
@ -27,18 +30,22 @@ export default function CheckoutForm() {
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: `https://${window.location.host}/payment/result`,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setMessage(error?.message || "Oops! Something went wrong.");
|
||||
}
|
||||
|
||||
dispatch(actions.status.update("subscribed"));
|
||||
setIsProcessing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form id="payment-form" onSubmit={handleSubmit}>
|
||||
<form
|
||||
className="payment-form-stripe"
|
||||
id="payment-form"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<PaymentElement />
|
||||
<MainButton color="blue" disabled={isProcessing} id="submit">
|
||||
<span id="button-text">
|
||||
|
||||
@ -6,6 +6,9 @@ import { Stripe, loadStripe } from "@stripe/stripe-js";
|
||||
import { Elements } from "@stripe/react-stripe-js";
|
||||
import CheckoutForm from "./CheckoutForm";
|
||||
import { useAuth } from "@/auth";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectors } from "@/store";
|
||||
import { PayPalReceiptPayload } from "@/api/resources/UserSubscriptionReceipts";
|
||||
|
||||
interface StripeModalProps {
|
||||
open: boolean;
|
||||
@ -22,10 +25,11 @@ export function StripeModal({
|
||||
StripeModalProps): JSX.Element {
|
||||
const api = useApi();
|
||||
const { token } = useAuth();
|
||||
const activeSubPlan = useSelector(selectors.selectActiveSubPlan);
|
||||
const [stripePromise, setStripePromise] =
|
||||
useState<Promise<Stripe | null> | null>(null);
|
||||
const [clientSecret, setClientSecret] = useState<string>("");
|
||||
const [isLoading, setIsLoading ] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@ -37,11 +41,14 @@ StripeModalProps): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
(async () => {
|
||||
|
||||
const { subscription_receipt } = await api.createSubscriptionReceipt({
|
||||
token,
|
||||
way: "paypal",
|
||||
itemInterval: "year",
|
||||
});
|
||||
subscription_receipt: {
|
||||
sub_plan_id: activeSubPlan?.id || "",
|
||||
},
|
||||
} as PayPalReceiptPayload);
|
||||
const { client_secret } = subscription_receipt.data;
|
||||
setClientSecret(client_secret);
|
||||
setIsLoading(false);
|
||||
|
||||
@ -1,14 +1,20 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import routes from '@/routes'
|
||||
import routes from "@/routes";
|
||||
import styles from "./styles.module.css";
|
||||
import Title from "@/components/Title";
|
||||
import MainButton from "@/components/MainButton";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { actions } from "@/store";
|
||||
|
||||
function PaymentSuccessPage(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate()
|
||||
const handleNext = () => navigate(routes.client.home())
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const handleNext = () => {
|
||||
dispatch(actions.status.update("subscribed"));
|
||||
navigate(routes.client.home());
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={`${styles.page} page`}>
|
||||
@ -17,7 +23,9 @@ function PaymentSuccessPage(): JSX.Element {
|
||||
<Title variant="h1">{t("auweb.pay_good.title")}</Title>
|
||||
<p>{t("auweb.pay_good.text1")}</p>
|
||||
</div>
|
||||
<MainButton className={styles.button} onClick={handleNext}>{t("auweb.pay_good.button")}</MainButton>
|
||||
<MainButton className={styles.button} onClick={handleNext}>
|
||||
{t("auweb.pay_good.button")}
|
||||
</MainButton>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,18 +2,22 @@ import { useNavigate } from "react-router-dom";
|
||||
import routes from "@/routes";
|
||||
import { useEffect } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { actions } from "@/store";
|
||||
|
||||
function PaymentResultPage(): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [searchParams] = useSearchParams();
|
||||
const status = searchParams.get("redirect_status");
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "succeeded") {
|
||||
dispatch(actions.status.update("subscribed"));
|
||||
return navigate(routes.client.paymentSuccess());
|
||||
}
|
||||
return navigate(routes.client.paymentFail());
|
||||
}, [navigate, status]);
|
||||
}, [navigate, status, dispatch]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
.payment-inforamtion {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: .0008em;
|
||||
letter-spacing: 0.0008em;
|
||||
}
|
||||
|
||||
.payment-chargebee {
|
||||
@ -82,6 +82,13 @@
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.payment-form-stripe {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.payment-loader {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -100,6 +107,14 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.payment-buttons-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pay-btn,
|
||||
.gpay-button-fake-loader,
|
||||
.apple-pay-button-placeholder {
|
||||
@ -138,6 +153,6 @@
|
||||
display: flex;
|
||||
align-items: end;
|
||||
background-repeat: no-repeat;
|
||||
background-color: rgba(0,0,0,.5);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
padding: 12px 15% 10px;
|
||||
}
|
||||
|
||||
@ -1,23 +1,27 @@
|
||||
import './styles.css'
|
||||
import "./styles.css";
|
||||
|
||||
interface PolicyProps {
|
||||
children: string
|
||||
sizing?: 'small' | 'medium' | 'large'
|
||||
className?: string
|
||||
children: string | JSX.Element | null;
|
||||
sizing?: "small" | "medium" | "large";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
small: 'policy--small',
|
||||
medium: 'policy--medium',
|
||||
large: 'policy--large',
|
||||
}
|
||||
small: "policy--small",
|
||||
medium: "policy--medium",
|
||||
large: "policy--large",
|
||||
};
|
||||
|
||||
function Policy({ children, sizing = 'small', className = '' }: PolicyProps): JSX.Element {
|
||||
function Policy({
|
||||
children,
|
||||
sizing = "small",
|
||||
className = "",
|
||||
}: PolicyProps): JSX.Element {
|
||||
return (
|
||||
<div className={`policy ${sizes[sizing]} ${className}`}>
|
||||
<p>{children}</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Policy
|
||||
export default Policy;
|
||||
|
||||
@ -1,29 +1,17 @@
|
||||
import { removeAfterDot, roundToWhole } from "@/services/price";
|
||||
import { Currency, Locale, Price } from "../PaymentTable";
|
||||
import { IPrice } from "../PriceList";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
const currency = Currency.USD;
|
||||
const locale = Locale.EN;
|
||||
|
||||
const roundToWhole = (value: string | number): number => {
|
||||
value = Number(value);
|
||||
if (value % Math.floor(value) !== 0) {
|
||||
return value;
|
||||
}
|
||||
return Math.floor(value);
|
||||
};
|
||||
|
||||
const removeAfterDot = (value: string): string => {
|
||||
const _value = Number(value.split("$")[1]);
|
||||
if (_value % Math.floor(_value) !== 0 && _value !== 0) {
|
||||
return value;
|
||||
}
|
||||
return value.split(".")[0];
|
||||
};
|
||||
|
||||
interface PriceItemProps {
|
||||
id: string,
|
||||
value: number,
|
||||
active: boolean;
|
||||
click: (id: number) => void;
|
||||
click: (id: string) => void;
|
||||
}
|
||||
|
||||
function PriceItem({
|
||||
@ -31,11 +19,11 @@ function PriceItem({
|
||||
value,
|
||||
active,
|
||||
click,
|
||||
}: IPrice & PriceItemProps): JSX.Element {
|
||||
}: PriceItemProps): JSX.Element {
|
||||
const _price = new Price(roundToWhole(value), currency, locale);
|
||||
|
||||
const compatClassName = () => {
|
||||
const isPopular = id === 3;
|
||||
const isPopular = id === 'stripe.7';
|
||||
const isActive = active;
|
||||
return `${styles.container} ${isPopular ? styles.popular : ""} ${isActive ? styles.active : ""}`;
|
||||
};
|
||||
|
||||
@ -1,64 +1,53 @@
|
||||
import { useState } from 'react'
|
||||
import PriceItem from '../PriceItem'
|
||||
import styles from './styles.module.css'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { actions } from '@/store'
|
||||
|
||||
export interface IPrice {
|
||||
id: number
|
||||
value: number
|
||||
}
|
||||
|
||||
const prices: IPrice[] = [
|
||||
{
|
||||
id: 1,
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
value: 5
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
value: 9
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
value: 13.67
|
||||
},
|
||||
]
|
||||
import { useState } from "react";
|
||||
import PriceItem from "../PriceItem";
|
||||
import styles from "./styles.module.css";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { actions } from "@/store";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
|
||||
interface PriceListProps {
|
||||
activeItem: number | null
|
||||
click: () => void
|
||||
subPlans: ISubscriptionPlan[];
|
||||
activeItem: number | null;
|
||||
click: () => void;
|
||||
}
|
||||
|
||||
function PriceList({click}: PriceListProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [activePriceItem, setActivePriceItem] = useState<number | null>(null)
|
||||
const getPrice = (plan: ISubscriptionPlan) => {
|
||||
return (plan.trial?.price_cents || 0) / 100;
|
||||
};
|
||||
|
||||
const priceItemClick = (id: number) => {
|
||||
setActivePriceItem(id)
|
||||
const activePriceItem = prices.find((item) => item.id === Number(id))
|
||||
if (activePriceItem) {
|
||||
function PriceList({ click, subPlans }: PriceListProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [activePlanItem, setActivePlanItem] =
|
||||
useState<ISubscriptionPlan | null>(null);
|
||||
|
||||
const priceItemClick = (id: string) => {
|
||||
const activePlan = subPlans.find((item) => item.id === String(id)) || null;
|
||||
setActivePlanItem(activePlan);
|
||||
if (activePlan) {
|
||||
dispatch(
|
||||
actions.payment.update({
|
||||
selectedPrice: activePriceItem.value
|
||||
activeSubPlan: activePlan,
|
||||
})
|
||||
);
|
||||
}
|
||||
setTimeout(() => {
|
||||
click()
|
||||
}, 1000)
|
||||
}
|
||||
click();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.container}`}>
|
||||
{prices.map((price, idx) => (
|
||||
<PriceItem active={price.id === activePriceItem} key={idx} value={price.value} id={price.id} click={priceItemClick} />
|
||||
{subPlans.map((plan, idx) => (
|
||||
<PriceItem
|
||||
active={plan.id === activePlanItem?.id}
|
||||
key={idx}
|
||||
value={getPrice(plan)}
|
||||
id={plan.id}
|
||||
click={priceItemClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default PriceList
|
||||
export default PriceList;
|
||||
|
||||
@ -1,46 +1,87 @@
|
||||
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'
|
||||
import { actions, selectors } from '@/store'
|
||||
import Title from '../Title'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import EmailsList from '../EmailsList'
|
||||
import PriceList from '../PriceList'
|
||||
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";
|
||||
import { actions, selectors } from "@/store";
|
||||
import Title from "../Title";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import EmailsList from "../EmailsList";
|
||||
import PriceList from "../PriceList";
|
||||
import { useApi } from "@/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import Loader from "../Loader";
|
||||
|
||||
function PriceListPage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { t, i18n } = useTranslation();
|
||||
const locale = i18n.language;
|
||||
const api = useApi();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const homeConfig = useSelector(selectors.selectHome);
|
||||
const selectedPrice = useSelector(selectors.selectSelectedPrice)
|
||||
|
||||
const email = useSelector(selectors.selectEmail)
|
||||
const selectedPrice = useSelector(selectors.selectSelectedPrice);
|
||||
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
|
||||
const email = useSelector(selectors.selectEmail);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { sub_plans } = await api.getSubscriptionPlans({ locale });
|
||||
const plans = sub_plans
|
||||
.filter(
|
||||
(plan: ISubscriptionPlan) => plan.provider === "stripe" && plan.trial
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (!a.trial || !b.trial) {
|
||||
return 0;
|
||||
}
|
||||
if (a.trial.price_cents < b.trial.price_cents) {
|
||||
return -1;
|
||||
}
|
||||
if (a.trial.price_cents > b.trial.price_cents) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
setSubPlans(plans);
|
||||
})();
|
||||
}, [api]);
|
||||
|
||||
const handleNext = () => {
|
||||
dispatch(
|
||||
actions.siteConfig.update({
|
||||
home: { pathFromHome: homeConfig.pathFromHome, isShowNavbar: false },
|
||||
})
|
||||
);
|
||||
navigate(routes.client.subscription())
|
||||
}
|
||||
navigate(routes.client.subscription());
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserHeader email={email} />
|
||||
<section className={`${styles.page} page`}>
|
||||
<Title className={styles.title} variant='h2'>{t('choose_your_own_fee')}</Title>
|
||||
<p className={styles.slogan}>{t('aura.web.price_selection')}</p>
|
||||
<div className={styles['emails-list-container']}>
|
||||
<EmailsList />
|
||||
<UserHeader email={email} />
|
||||
<section className={`${styles.page} page`}>
|
||||
{!!subPlans.length && (
|
||||
<>
|
||||
<Title className={styles.title} variant="h2">
|
||||
{t("choose_your_own_fee")}
|
||||
</Title>
|
||||
<p className={styles.slogan}>{t("aura.web.price_selection")}</p>
|
||||
<div className={styles["emails-list-container"]}>
|
||||
<EmailsList />
|
||||
</div>
|
||||
<div className={styles['price-list-container']}>
|
||||
<PriceList activeItem={selectedPrice} click={handleNext} />
|
||||
<div className={styles["price-list-container"]}>
|
||||
<PriceList
|
||||
activeItem={selectedPrice}
|
||||
subPlans={subPlans}
|
||||
click={handleNext}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
{!subPlans.length && <Loader />}
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default PriceListPage
|
||||
export default PriceListPage;
|
||||
|
||||
@ -33,6 +33,10 @@ function SpecialWelcomeOffer({ open, onClose }: ModalTopProps): JSX.Element {
|
||||
navigate(routes.client.paymentMethod());
|
||||
};
|
||||
|
||||
const handleMoreAbout = () => {
|
||||
window.location.href = "https://witapps.us/en/aura";
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{open ? (
|
||||
@ -69,9 +73,7 @@ function SpecialWelcomeOffer({ open, onClose }: ModalTopProps): JSX.Element {
|
||||
<MainButton
|
||||
// disabled
|
||||
className={styles["button-black"]}
|
||||
onClick={() => {
|
||||
console.log("click");
|
||||
}}
|
||||
onClick={handleMoreAbout}
|
||||
>
|
||||
<img className={styles["button-icon"]} src="/leo.png" alt="Leo" />
|
||||
{t("au.more_llc.button")}
|
||||
|
||||
63
src/components/StripePage/index.tsx
Normal file
63
src/components/StripePage/index.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { useApi } from "@/api";
|
||||
import Loader from "@/components/Loader";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Stripe, loadStripe } from "@stripe/stripe-js";
|
||||
import { Elements } from "@stripe/react-stripe-js";
|
||||
import CheckoutForm from "../PaymentPage/methods/Stripe/CheckoutForm";
|
||||
import { useAuth } from "@/auth";
|
||||
import styles from "./styles.module.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectors } from "@/store";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import routes from "@/routes";
|
||||
|
||||
export function StripePage(): JSX.Element {
|
||||
const api = useApi();
|
||||
const { token } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const activeSubPlan = useSelector(selectors.selectActiveSubPlan);
|
||||
const [stripePromise, setStripePromise] =
|
||||
useState<Promise<Stripe | null> | null>(null);
|
||||
const [clientSecret, setClientSecret] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
if (!activeSubPlan) {
|
||||
navigate(routes.client.priceList());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const siteConfig = await api.getAppConfig({ bundleId: "auraweb" });
|
||||
setStripePromise(loadStripe(siteConfig.data.stripe_public_key));
|
||||
})();
|
||||
}, [api]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { subscription_receipt } = await api.createSubscriptionReceipt({
|
||||
token,
|
||||
way: "stripe",
|
||||
subscription_receipt: {
|
||||
sub_plan_id: activeSubPlan?.id || "stripe.7",
|
||||
},
|
||||
});
|
||||
const { client_secret } = subscription_receipt.data;
|
||||
setClientSecret(client_secret);
|
||||
setIsLoading(false);
|
||||
})();
|
||||
}, [api, token]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.page} page`}>
|
||||
{isLoading ? (
|
||||
<div className={styles["payment-loader"]}>
|
||||
<Loader />
|
||||
</div>
|
||||
) : null}
|
||||
{stripePromise && clientSecret && (
|
||||
<Elements stripe={stripePromise} options={{ clientSecret }}>
|
||||
<CheckoutForm />
|
||||
</Elements>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
src/components/StripePage/styles.module.css
Normal file
16
src/components/StripePage/styles.module.css
Normal file
@ -0,0 +1,16 @@
|
||||
.page {
|
||||
position: relative;
|
||||
/* height: calc(100vh - 50px);
|
||||
max-height: -webkit-fill-available; */
|
||||
flex: auto;
|
||||
justify-content: center;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 96px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.payment-loader {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@ -13,28 +13,34 @@ import styles from "./styles.module.css";
|
||||
import Header from "../Header";
|
||||
import SpecialWelcomeOffer from "../SpecialWelcomeOffer";
|
||||
import { useState } from "react";
|
||||
import { ITrial } from "@/api/resources/SubscriptionPlans";
|
||||
|
||||
const currency = Currency.USD;
|
||||
const locale = Locale.EN;
|
||||
const itemPriceId = "aura-membership-2-week-USD";
|
||||
|
||||
const getPriceFromTrial = (trial: ITrial | null) => {
|
||||
if (!trial) {
|
||||
return 0;
|
||||
}
|
||||
return (trial.price_cents || 0) / 100;
|
||||
};
|
||||
|
||||
function SubscriptionPage(): JSX.Element {
|
||||
const [isOpenModal, setIsOpenModal] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const activeSubPlan = useSelector(selectors.selectActiveSubPlan);
|
||||
const paymentItems = [
|
||||
{
|
||||
title: "Per 7-Day Trial For",
|
||||
price: 1.0,
|
||||
description: t("au.2week_plan.web"),
|
||||
title: activeSubPlan?.name || "Per 7-Day Trial For",
|
||||
price: getPriceFromTrial(activeSubPlan?.trial || null),
|
||||
description: activeSubPlan?.desc.length
|
||||
? activeSubPlan?.desc
|
||||
: t("au.2week_plan.web"),
|
||||
},
|
||||
];
|
||||
|
||||
const navigate = useNavigate();
|
||||
const email = useSelector(selectors.selectEmail);
|
||||
const itemPrice = useSelector(selectors.selectPlanById(itemPriceId));
|
||||
const selectedPrice = useSelector(selectors.selectSelectedPrice);
|
||||
if (selectedPrice || selectedPrice === 0) {
|
||||
paymentItems[0].price = selectedPrice;
|
||||
}
|
||||
const handleClick = () => navigate(routes.client.paymentMethod());
|
||||
const handleCross = () => setIsOpenModal(true);
|
||||
const policyLink = (
|
||||
@ -42,13 +48,13 @@ function SubscriptionPage(): JSX.Element {
|
||||
{t("subscription_policy")}
|
||||
</a>
|
||||
);
|
||||
console.log({ itemPrice });
|
||||
|
||||
return (
|
||||
<>
|
||||
<SpecialWelcomeOffer open={isOpenModal} onClose={handleClick} />
|
||||
<Header classCross={styles.cross} clickCross={handleCross} />
|
||||
<UserHeader email={email} />
|
||||
<section className="page">
|
||||
<section className={`${styles.page} page`}>
|
||||
<CallToAction />
|
||||
<Countdown start={10} />
|
||||
<PaymentTable
|
||||
@ -59,7 +65,12 @@ function SubscriptionPage(): JSX.Element {
|
||||
<div className={styles["subscription-action"]}>
|
||||
<MainButton onClick={handleClick}>{t("get_access")}</MainButton>
|
||||
</div>
|
||||
<Policy>{t("subscription_text", { policyLink })}</Policy>
|
||||
<Policy>
|
||||
<>
|
||||
{t("auweb.agree.text1")}
|
||||
{t("subscription_text", { policyLink })}
|
||||
</>
|
||||
</Policy>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
.page {
|
||||
padding-bottom: 32px !important;
|
||||
}
|
||||
|
||||
.subscription-action {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
@ -5,7 +9,7 @@
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
background-color: transparent;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
|
||||
@ -47,7 +47,9 @@ function getZodiacParagraphs(
|
||||
{paragraph.title}
|
||||
</Headline>
|
||||
{getTypeOfContent(paragraph.content) === "string"
|
||||
? paragraph.content.map((content, _index) => <p key={_index}>{content as string}</p>)
|
||||
? paragraph.content.map((content, _index) => (
|
||||
<p key={_index}>{content as string}</p>
|
||||
))
|
||||
: getZodiacParagraphs(paragraph.content as ZodiacParagraph[], depth)}
|
||||
</div>
|
||||
);
|
||||
@ -58,7 +60,7 @@ function WallpaperPage(): JSX.Element {
|
||||
const api = useApi();
|
||||
const { i18n } = useTranslation();
|
||||
const locale = i18n.language;
|
||||
const token = useSelector(selectors.selectToken)
|
||||
const token = useSelector(selectors.selectToken);
|
||||
|
||||
const {
|
||||
user,
|
||||
@ -111,20 +113,6 @@ function WallpaperPage(): JSX.Element {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* <h1 className={styles["wallpaper-title"]}>
|
||||
{t("analysis_background")}
|
||||
</h1> */}
|
||||
{/* {forecasts.map((forecast) => (
|
||||
<div
|
||||
key={forecast.category_name}
|
||||
className={styles["wallpaper-forecast"]}
|
||||
>
|
||||
<h2 className={styles["wallpaper-subtitle"]}>
|
||||
{forecast.category}
|
||||
</h2>
|
||||
<p className={styles["wallpaper-text"]}>{forecast.body}</p>
|
||||
</div>
|
||||
))} */}
|
||||
<div className={styles["wallpaper-forecast"]}>
|
||||
{getZodiacParagraphs(zodiacInfo?.paragraphs || [])}
|
||||
</div>
|
||||
|
||||
@ -35,7 +35,7 @@ export default {
|
||||
charged_only: "You will be charged only <price> for your 7-day trial. We'll email you a reminder before your trial period ends. Cancel anytime.",
|
||||
purposes: "For entertaiment purposes only.",
|
||||
get_access: "Get access",
|
||||
subscription_text: "By proceeding, you agree that if you do not cancel your subscription before the end of the 7-day trial period, you will be automatically charged nineteen US dollars zero cents every 2 weeks until you cancel the subscription in the settings. Learn more about cancellation and refund policy in <policyLink>",
|
||||
subscription_text: " Learn more about cancellation and refund policy in <policyLink>",
|
||||
subscription_policy: "Subscription policy",
|
||||
company_name: "Wit Apps LLC, California, US",
|
||||
choose_payment: "Choose Payment Method",
|
||||
@ -81,6 +81,7 @@ export default {
|
||||
you_and: "You and <user>",
|
||||
sign: "Sign",
|
||||
stripe: "Stripe",
|
||||
payPal: "PayPal",
|
||||
'aura-10_breath-button': "Increase up to 10%. Practice for the Energy of Money",
|
||||
'aura-money_compatibility-button': "low MONEY energy. Determine who drains your energy",
|
||||
"breathe-subtitle": "Breathing practice will help improve your aura. Breath in the positive energy, breathe out the negative...",
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import type { UserStatus } from "./types";
|
||||
|
||||
const host = "";
|
||||
const apiHost = "https://aura.wit.life";
|
||||
export const apiHost = "https://api-web.aura.wit.life";
|
||||
const siteHost = "https://aura.wit.life";
|
||||
const prefix = "api/v1";
|
||||
|
||||
const routes = {
|
||||
@ -12,6 +13,8 @@ const routes = {
|
||||
freePeriodInfo: () => [host, "free-period"].join("/"),
|
||||
birthtime: () => [host, "birthtime"].join("/"),
|
||||
emailEnter: () => [host, "email"].join("/"),
|
||||
authResult: () => [host, "auth", "result"].join("/"),
|
||||
auth: () => [host, "auth"].join("/"),
|
||||
subscription: () => [host, "subscription"].join("/"),
|
||||
createProfile: () => [host, "profile", "create"].join("/"),
|
||||
attention: () => [host, "attention"].join("/"),
|
||||
@ -20,6 +23,7 @@ const routes = {
|
||||
paymentResult: () => [host, "payment", "result"].join("/"),
|
||||
paymentSuccess: () => [host, "payment", "success"].join("/"),
|
||||
paymentFail: () => [host, "payment", "fail"].join("/"),
|
||||
paymentStripe: () => [host, "payment", "stripe"].join("/"),
|
||||
wallpaper: () => [host, "wallpaper"].join("/"),
|
||||
static: () => [host, "static", ":typeId"].join("/"),
|
||||
legal: (type: string) => [host, "static", type].join("/"),
|
||||
@ -31,10 +35,13 @@ const routes = {
|
||||
breathResult: () => [host, "breath", "result"].join("/"),
|
||||
},
|
||||
server: {
|
||||
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("/"),
|
||||
zodiacs: (zodiac: string) => [apiHost, prefix, "zodiacs", `${zodiac}.json`].join("/"),
|
||||
zodiacs: (zodiac: string) =>
|
||||
[apiHost, prefix, "zodiacs", `${zodiac}.json`].join("/"),
|
||||
element: (type: string) =>
|
||||
[apiHost, prefix, "elements", `${type}.json`].join("/"),
|
||||
apps: (bundleId: string) =>
|
||||
@ -50,6 +57,7 @@ const routes = {
|
||||
[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(
|
||||
"/"
|
||||
@ -71,7 +79,7 @@ const routes = {
|
||||
[apiHost, prefix, "user", "callbacks.json"].join("/"),
|
||||
getUserCallbacks: (id: string) =>
|
||||
[apiHost, prefix, "user", "callbacks", `${id}.json`].join("/"),
|
||||
getTranslations: () => [apiHost, "api/v2", "t.json"].join("/"),
|
||||
getTranslations: () => [siteHost, "api/v2", "t.json"].join("/"),
|
||||
},
|
||||
};
|
||||
|
||||
@ -101,7 +109,7 @@ export const hasNoNavigation = (path: string) => !hasNavigation(path);
|
||||
export const withCrossButtonRoutes = [
|
||||
// routes.client.attention(),
|
||||
routes.client.subscription(),
|
||||
routes.client.paymentMethod()
|
||||
routes.client.paymentMethod(),
|
||||
];
|
||||
export const hasCrossButton = (path: string) =>
|
||||
withCrossButtonRoutes.includes(path);
|
||||
@ -121,6 +129,7 @@ export const withoutFooterRoutes = [
|
||||
routes.client.paymentResult(),
|
||||
routes.client.paymentSuccess(),
|
||||
routes.client.paymentFail(),
|
||||
routes.client.paymentStripe(),
|
||||
];
|
||||
export const hasNoFooter = (path: string) =>
|
||||
!withoutFooterRoutes.includes(path);
|
||||
@ -155,7 +164,7 @@ export const getRouteBy = (status: UserStatus): string => {
|
||||
case "unsubscribed":
|
||||
return routes.client.subscription();
|
||||
case "subscribed":
|
||||
return routes.client.wallpaper();
|
||||
return routes.client.home();
|
||||
default:
|
||||
throw new Error(`Unknown user status, received status is "${status}"`);
|
||||
}
|
||||
|
||||
15
src/services/price/index.ts
Normal file
15
src/services/price/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export const roundToWhole = (value: string | number): number => {
|
||||
value = Number(value);
|
||||
if (value % Math.floor(value) !== 0) {
|
||||
return value;
|
||||
}
|
||||
return Math.floor(value);
|
||||
};
|
||||
|
||||
export const removeAfterDot = (value: string): string => {
|
||||
const _value = Number(value.split("$")[1]);
|
||||
if (_value % Math.floor(_value) !== 0 && _value !== 0) {
|
||||
return value;
|
||||
}
|
||||
return value.split(".")[0];
|
||||
};
|
||||
@ -24,6 +24,7 @@ import onboardingConfig, {
|
||||
} from "./onboarding";
|
||||
import payment, {
|
||||
actions as paymentActions,
|
||||
selectActiveSubPlan,
|
||||
selectIsDiscount,
|
||||
} from "./payment";
|
||||
import subscriptionPlans, {
|
||||
@ -71,6 +72,7 @@ export const selectors = {
|
||||
selectSelfName,
|
||||
selectCategoryId,
|
||||
selectSelectedPrice,
|
||||
selectActiveSubPlan,
|
||||
selectUserCallbacksDescription,
|
||||
selectUserCallbacksPrevStat,
|
||||
selectHome,
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
|
||||
import { createSlice, createSelector } from "@reduxjs/toolkit";
|
||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
interface IPayment {
|
||||
selectedPrice: number | null;
|
||||
isDiscount: boolean;
|
||||
activeSubPlan: ISubscriptionPlan | null;
|
||||
}
|
||||
|
||||
const initialState: IPayment = {
|
||||
selectedPrice: null,
|
||||
isDiscount: false,
|
||||
activeSubPlan: null
|
||||
};
|
||||
|
||||
const paymentSlice = createSlice({
|
||||
@ -27,6 +30,10 @@ export const selectSelectedPrice = createSelector(
|
||||
(state: { payment: IPayment }) => state.payment.selectedPrice,
|
||||
(payment) => payment
|
||||
);
|
||||
export const selectActiveSubPlan = createSelector(
|
||||
(state: { payment: IPayment }) => state.payment.activeSubPlan,
|
||||
(payment) => payment
|
||||
);
|
||||
export const selectIsDiscount = createSelector(
|
||||
(state: { payment: IPayment }) => state.payment.isDiscount,
|
||||
(payment) => payment
|
||||
|
||||
26
src/types.ts
26
src/types.ts
@ -1,8 +1,8 @@
|
||||
import { Chargebee } from '@chargebee/chargebee-js-types'
|
||||
import { Chargebee } from "@chargebee/chargebee-js-types";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Chargebee: typeof Chargebee
|
||||
Chargebee: typeof Chargebee;
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,19 +14,19 @@ export enum EDirectionOnboarding {
|
||||
}
|
||||
|
||||
export interface FormField<T> {
|
||||
name: string
|
||||
value: T
|
||||
label?: string | null
|
||||
placeholder?: string | null
|
||||
inputClassName?: string
|
||||
onValid: (value: string) => void
|
||||
onInvalid: () => void
|
||||
name: string;
|
||||
value: T;
|
||||
label?: string | null;
|
||||
placeholder?: string | null;
|
||||
inputClassName?: string;
|
||||
onValid: (value: string) => void;
|
||||
onInvalid: () => void;
|
||||
}
|
||||
|
||||
export interface SignupForm {
|
||||
email: string
|
||||
birthdate: string
|
||||
birthtime: string
|
||||
email: string;
|
||||
birthdate: string;
|
||||
birthtime: string;
|
||||
}
|
||||
|
||||
export type UserStatus = 'lead' | 'registred' | 'subscribed' | 'unsubscribed'
|
||||
export type UserStatus = "lead" | "registred" | "subscribed" | "unsubscribed";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user