feat: add authorization with apple and google, add payment systems, minor edits, fixed bugs

This commit is contained in:
gofnnp 2023-11-16 02:29:49 +04:00
parent 5b856aea74
commit 8db05c3fd4
58 changed files with 1150 additions and 513 deletions

BIN
public/apple-auth-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

@ -19,11 +19,16 @@ import {
AIRequests, AIRequests,
UserCallbacks, UserCallbacks,
Translations, Translations,
Zodiacs Zodiacs,
GoogleAuth,
SubscriptionPlans,
AppleAuth
} from './resources' } from './resources'
const api = { const api = {
auth: createMethod<AuthTokens.Payload, AuthTokens.Response>(AuthTokens.createRequest), 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), 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), getElements: createMethod<Elements.Payload, Elements.Response>(Elements.createRequest),
@ -34,6 +39,7 @@ const api = {
getDailyForecasts: createMethod<DailyForecasts.Payload, DailyForecasts.Response>(DailyForecasts.createRequest), getDailyForecasts: createMethod<DailyForecasts.Payload, DailyForecasts.Response>(DailyForecasts.createRequest),
getAuras: createMethod<Auras.Payload, Auras.Response>(Auras.createRequest), getAuras: createMethod<Auras.Payload, Auras.Response>(Auras.createRequest),
getSubscriptionItems: createMethod<SubscriptionItems.Payload, SubscriptionItems.Response>(SubscriptionItems.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), getSubscriptionCheckout: createMethod<SubscriptionCheckout.Payload, SubscriptionCheckout.Response>(SubscriptionCheckout.createRequest),
getSubscriptionStatus: createMethod<SubscriptionStatus.Payload, SubscriptionStatus.Response>(SubscriptionStatus.createRequest), getSubscriptionStatus: createMethod<SubscriptionStatus.Payload, SubscriptionStatus.Response>(SubscriptionStatus.createRequest),
getSubscriptionReceipt: createMethod<SubscriptionReceipts.GetPayload, SubscriptionReceipts.Response>(SubscriptionReceipts.createGetRequest), getSubscriptionReceipt: createMethod<SubscriptionReceipts.GetPayload, SubscriptionReceipts.Response>(SubscriptionReceipts.createGetRequest),

View File

@ -1,3 +1,4 @@
import { apiHost } from "@/routes";
import { getAuthHeaders } from "../utils"; import { getAuthHeaders } from "../utils";
export interface Payload { export interface Payload {
@ -40,6 +41,6 @@ export interface IAiInputs {
} }
export const createRequest = ({ body_check_path, token }: Payload): Request => { 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) }); return new Request(url, { method: "GET", headers: getAuthHeaders(token) });
}; };

View 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" });
};

View File

@ -1,6 +1,4 @@
import routes from "@/routes" import routes from "@/routes"
// import { AuthPayload } from "../types"
// import { getAuthHeaders } from "../utils"
export interface Payload { export interface Payload {
locale: string locale: string

View File

@ -1,9 +1,6 @@
import routes from "@/routes" import routes from "@/routes"
import { AssetCategory } from "./AssetCategories" import { AssetCategory } from "./AssetCategories"
// import { AuthPayload } from "../types"
// import { getAuthHeaders } from "../utils"
// export interface Payload extends AuthPayload {
export interface Payload { export interface Payload {
category: string category: string
page?: number page?: number

View 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",
});
};

View 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" });
};

View File

@ -1,121 +1,132 @@
import routes from "@/routes" import routes from "@/routes";
import { AuthPayload } from "../types" import { AuthPayload } from "../types";
import { getAuthHeaders } from "../utils" import { getAuthHeaders } from "../utils";
export type GetPayload = AuthPayload export type GetPayload = AuthPayload;
export interface PatchPayload extends AuthPayload { export interface PatchPayload extends AuthPayload {
user: Partial<UserPatch> user: Partial<UserPatch>;
} }
export interface Response { export interface Response {
user: User user: User;
meta?: { meta?: {
links: { links: {
self: string self: string;
} };
} };
} }
export interface UserPatch { export interface UserPatch {
locale: string locale: string;
timezone: string timezone: string;
profile_attributes: Partial<Pick<UserProfile, 'gender' | 'full_name' | 'relationship_status' | 'birthday'> & { profile_attributes: Partial<
birthplace_id: null Pick<
birthplace_attributes: { UserProfile,
address?: string "gender" | "full_name" | "relationship_status" | "birthday"
coords?: string > & {
}, birthplace_id: null;
remote_userpic_url: string birthplace_attributes: {
}> address?: string;
daily_push_subs_attributes: [{ coords?: string;
time: string };
daily_push_id: string remote_userpic_url: string;
}] }
>;
daily_push_subs_attributes: [
{
time: string;
daily_push_id: string;
}
];
} }
export interface User { export interface User {
id: string | null | undefined id: string | null | undefined;
username: string | null username: string | null;
email: string email: string;
locale: string locale: string;
state: string state: string;
timezone: string timezone: string;
new_registration: boolean new_registration: boolean;
stat: { stat: {
last_online_at: string | null last_online_at: string | null;
prev_online_at: string | null prev_online_at: string | null;
} };
profile: UserProfile profile: UserProfile;
daily_push_subs: Subscription[] daily_push_subs: Subscription[];
} }
export interface UserProfile { export interface UserProfile {
full_name: string | null full_name: string | null;
gender: string | null gender: string | null;
birthday: string | null birthday: string | null;
birthplace: UserBirhplace | null birthplace: UserBirhplace | null;
age: UserAge | null age: UserAge | null;
sign: UserSign | null sign: UserSign | null;
userpic: UserPic | null userpic: UserPic | null;
userpic_mime_type: string | undefined userpic_mime_type: string | undefined;
relationship_status: string relationship_status: string;
human_relationship_status: string human_relationship_status: string;
} }
export interface UserAge { export interface UserAge {
years: number years: number;
days: number days: number;
} }
export interface UserSign { export interface UserSign {
house: number house: number;
ruler: string ruler: string;
dates: { dates: {
start: { start: {
month: number month: number;
day: number day: number;
} };
end: { end: {
month: number month: number;
day: number day: number;
} };
} };
sign: string sign: string;
char: string char: string;
polarity: string polarity: string;
modality: string modality: string;
triplicity: string triplicity: string;
} }
export interface UserPic { export interface UserPic {
th: string th: string;
th2x: string th2x: string;
lg: string lg: string;
} }
export interface UserBirhplace { export interface UserBirhplace {
id: string id: string;
address: string address: string;
coords: string coords: string;
} }
export interface Subscription { export interface Subscription {
id: string id: string;
daily_push_id: string daily_push_id: string;
time: string time: string;
updated_at: string updated_at: string;
created_at: string created_at: string;
last_sent_at: string | null last_sent_at: string | null;
} }
export const createGetRequest = ({ token }: GetPayload): Request => { export const createGetRequest = ({ token }: GetPayload): Request => {
const url = new URL(routes.server.user()) const url = new URL(routes.server.user());
return new Request(url, { method: 'GET', headers: getAuthHeaders(token) }) return new Request(url, { method: "GET", headers: getAuthHeaders(token) });
} };
export const createPatchRequest = ({ token, user }: PatchPayload): Request => { export const createPatchRequest = ({ token, user }: PatchPayload): Request => {
const url = new URL(routes.server.user()) const url = new URL(routes.server.user());
const body = JSON.stringify({ user }) const body = JSON.stringify({ user });
return new Request(url, { method: 'PATCH', headers: getAuthHeaders(token), body }) return new Request(url, {
} method: "PATCH",
headers: getAuthHeaders(token),
body,
});
};

View File

@ -19,13 +19,22 @@ export interface AppleReceiptPayload extends AuthPayload {
} }
export interface StripeReceiptPayload extends AuthPayload { export interface StripeReceiptPayload extends AuthPayload {
way: "stripe";
subscription_receipt: {
sub_plan_id: string;
};
}
export interface PayPalReceiptPayload extends AuthPayload {
itemInterval: "week" | "month" | "year"; itemInterval: "week" | "month" | "year";
way: "paypal";
} }
export type Payload = export type Payload =
| ChargebeeReceiptPayload | ChargebeeReceiptPayload
| AppleReceiptPayload | AppleReceiptPayload
| StripeReceiptPayload; | StripeReceiptPayload
| PayPalReceiptPayload;
export interface Response { export interface Response {
subscription_receipt: SubscriptionReceipt; subscription_receipt: SubscriptionReceipt;
@ -54,9 +63,16 @@ export interface SubscriptionReceipt {
app_bundle_id: string; app_bundle_id: string;
autorenewable: boolean; autorenewable: boolean;
error: string; error: string;
links?: IPayPalLink[];
}; };
} }
interface IPayPalLink {
href: string;
rel: "approve" | "edit" | "self";
method: "GET" | "PATCH";
}
function createRequest({ function createRequest({
token, token,
itemPriceId, itemPriceId,
@ -69,7 +85,7 @@ function createRequest({
autorenewable = true, autorenewable = true,
sandbox = true, sandbox = true,
}: AppleReceiptPayload): Request; }: AppleReceiptPayload): Request;
function createRequest({ token, itemInterval }: StripeReceiptPayload): Request; function createRequest({ token }: StripeReceiptPayload): Request;
function createRequest(payload: Payload): Request; function createRequest(payload: Payload): Request;
function createRequest(payload: Payload): Request { function createRequest(payload: Payload): Request {
const url = new URL(routes.server.subscriptionReceipts()); 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 { return {
way: "stripe", way: "stripe",
subscription_receipt: { subscription_receipt: {
item_interval: payload.itemInterval, sub_plan_id: payload.subscription_receipt.sub_plan_id,
}, },
}; };
} }

View File

@ -1,20 +1,23 @@
export * as Assets from './Assets' export * as Assets from "./Assets";
export * as AssetCategories from './AssetCategories' export * as AssetCategories from "./AssetCategories";
export * as Apps from './Apps' export * as Apps from "./Apps";
export * as User from './User' export * as User from "./User";
export * as DailyForecasts from './UserDailyForecasts' export * as DailyForecasts from "./UserDailyForecasts";
export * as Auras from './Auras' export * as Auras from "./Auras";
export * as Element from './Element' export * as Element from "./Element";
export * as Elements from './Elements' export * as Elements from "./Elements";
export * as AuthTokens from './AuthTokens' export * as AuthTokens from "./AuthTokens";
export * as SubscriptionItems from './UserSubscriptionItemPrices' export * as SubscriptionItems from "./UserSubscriptionItemPrices";
export * as SubscriptionCheckout from './UserSubscriptionCheckout' export * as SubscriptionCheckout from "./UserSubscriptionCheckout";
export * as SubscriptionStatus from './UserSubscriptionStatus' export * as SubscriptionStatus from "./UserSubscriptionStatus";
export * as SubscriptionReceipts from './UserSubscriptionReceipts' export * as SubscriptionReceipts from "./UserSubscriptionReceipts";
export * as PaymentIntents from './UserPaymentIntents' export * as PaymentIntents from "./UserPaymentIntents";
export * as AICompatCategories from './AICompatCategories' export * as AICompatCategories from "./AICompatCategories";
export * as AICompats from './AICompats' export * as AICompats from "./AICompats";
export * as AIRequests from './AIRequests' export * as AIRequests from "./AIRequests";
export * as UserCallbacks from './UserCallbacks' export * as UserCallbacks from "./UserCallbacks";
export * as Translations from './Translations' export * as Translations from "./Translations";
export * as Zodiacs from './Zodiacs' export * as Zodiacs from "./Zodiacs";
export * as GoogleAuth from "./GoogleAuth";
export * as SubscriptionPlans from "./SubscriptionPlans";
export * as AppleAuth from "./AppleAuth";

View File

@ -1,4 +1,4 @@
import { useContext } from 'react' import { useContext } from "react";
import { AuthContext } from './AuthContext' import { AuthContext } from "./AuthContext";
export const useAuth = () => useContext(AuthContext) export const useAuth = () => useContext(AuthContext);

View File

@ -48,17 +48,22 @@ import { Asset } from "@/api/resources/Assets";
import PaymentResultPage from "../PaymentPage/results"; import PaymentResultPage from "../PaymentPage/results";
import PaymentSuccessPage from "../PaymentPage/results/SuccessPage"; import PaymentSuccessPage from "../PaymentPage/results/SuccessPage";
import PaymentFailPage from "../PaymentPage/results/ErrorPage"; import PaymentFailPage from "../PaymentPage/results/ErrorPage";
import { StripePage } from "../StripePage";
import AuthPage from "../AuthPage";
import AuthResultPage from "../AuthResultPage";
function App(): JSX.Element { function App(): JSX.Element {
const [isSpecialOfferOpen, setIsSpecialOfferOpen] = useState<boolean>(false); const [isSpecialOfferOpen, setIsSpecialOfferOpen] = useState<boolean>(false);
const [leoApng, setLeoApng] = useState<Error | APNG>(Error); const [leoApng, setLeoApng] = useState<Error | APNG>(Error);
const [padLockApng, setPadLockApng] = useState<Error | APNG>(Error);
const navigate = useNavigate(); const navigate = useNavigate();
const api = useApi(); const api = useApi();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { token, user } = useAuth();
const closeSpecialOfferAttention = () => { const closeSpecialOfferAttention = () => {
setIsSpecialOfferOpen(false); setIsSpecialOfferOpen(false);
navigate(routes.client.emailEnter()); navigate(routes.client.auth());
}; };
const assetsData = useCallback(async () => { const assetsData = useCallback(async () => {
@ -71,13 +76,25 @@ function App(): JSX.Element {
const { data } = useApiCall<Asset[]>(assetsData); const { data } = useApiCall<Asset[]>(assetsData);
useEffect(() => { useEffect(() => {
// TODO: remove later (async () => {
dispatch( if (!token.length || !user) return;
actions.token.update( const {
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjQwNjEyLCJpYXQiOjE2OTc5MjY0MTksImV4cCI6MTcwNjU2NjQxOSwianRpIjoiZTg0NWE0ZmUtYmVmNy00ODNmLWIwMzgtYjlkYzBlZjk1MjNmIiwiZW1haWwiOiJvdGhlcjJAZXhhbXBsZS5jb20iLCJzdGF0ZSI6InByb3ZlbiIsImxvYyI6ImVuIiwidHoiOjAsInR5cGUiOiJlbWFpbCIsImlzcyI6ImNvbS5saWZlLmF1cmEifQ.ijaHDiNRLUIKdkziVB-zt8DA8WNH7RNwvYkp2EGDxTM" user: { has_subscription },
) } = await api.getSubscriptionStatus({
); token,
}, [dispatch]); });
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(() => { useEffect(() => {
async function getApng() { async function getApng() {
@ -91,61 +108,115 @@ function App(): JSX.Element {
getApng(); getApng();
}, [data]); }, [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 ( return (
<Routes> <Routes>
<Route element={<Layout setIsSpecialOfferOpen={setIsSpecialOfferOpen} />}> <Route element={<Layout setIsSpecialOfferOpen={setIsSpecialOfferOpen} />}>
<Route path={routes.client.root()} element={<MainPage />} /> <Route element={<AuthorizedUserOutlet />}>
<Route path={routes.client.birthday()} element={<BirthdayPage />} /> <Route path={routes.client.root()} element={<MainPage />} />
<Route path={routes.client.didYouKnow()} element={<DidYouKnowPage />} /> <Route path={routes.client.birthday()} element={<BirthdayPage />} />
<Route <Route
path={routes.client.freePeriodInfo()} path={routes.client.didYouKnow()}
element={<FreePeriodInfoPage />} element={<DidYouKnowPage />}
/> />
<Route <Route
path={routes.client.attention()} path={routes.client.freePeriodInfo()}
element={ element={<FreePeriodInfoPage />}
<AttentionPage />
isOpenModal={isSpecialOfferOpen} <Route
onCloseSpecialOffer={closeSpecialOfferAttention} path={routes.client.attention()}
/> element={
} <AttentionPage
/> isOpenModal={isSpecialOfferOpen}
<Route path={routes.client.feedback()} element={<FeedbackPage />} /> onCloseSpecialOffer={closeSpecialOfferAttention}
<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.feedback()} element={<FeedbackPage />} />
<Route <Route path={routes.client.birthtime()} element={<BirthtimePage />} />
path={routes.client.compatibility()} <Route path={routes.client.createProfile()} element={<SkipStep />} />
element={<CompatibilityPage />} <Route
/> path={routes.client.emailEnter()}
<Route element={<EmailEnterPage />}
path={routes.client.compatibilityResult()} />
element={<CompatResultPage />} <Route
/> path={routes.client.auth()}
<Route element={<AuthPage padLockApng={padLockApng} />}
path={routes.client.breath()} />
element={<BreathPage leoApng={leoApng} />} <Route
/> path={routes.client.authResult()}
<Route path={routes.client.priceList()} element={<PriceListPage />} /> element={<AuthResultPage />}
<Route path={routes.client.home()} element={<HomePage />} /> />
<Route <Route path={routes.client.static()} element={<StaticPage />} />
path={routes.client.breathResult()} <Route path={routes.client.priceList()} element={<PriceListPage />} />
element={<UserCallbacksPage />} {/* <Route
/>
<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
path={routes.client.wallpaper()} path={routes.client.wallpaper()}
element={<ProtectWallpaperPage />} 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 path="*" element={<NotFoundPage />} />
</Route> </Route>
</Routes> </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 { function PrivateOutlet(): JSX.Element {
const { user } = useAuth(); const { user } = useAuth();
return user ? ( 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 { function SkipStep(): JSX.Element {
const { user } = useAuth(); const { user } = useAuth();
return user ? ( return user ? (
@ -271,14 +361,4 @@ function MainPage(): JSX.Element {
return <Navigate to={getRouteBy(status)} replace={true} />; 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; export default App;

View File

@ -1,43 +1,40 @@
import { useNavigate } from 'react-router-dom' import { useNavigate } from "react-router-dom";
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next";
import Title from '../Title' import Title from "../Title";
import routes from '@/routes' import routes from "@/routes";
import styles from './styles.module.css' import styles from "./styles.module.css";
// import CheckboxWithText from '../CheckboxWithText' import SpecialWelcomeOffer from "../SpecialWelcomeOffer";
import SpecialWelcomeOffer from '../SpecialWelcomeOffer' import MainButton from "../MainButton";
import MainButton from '../MainButton'
// import MainButton from '../MainButton'
interface AttentionPageProps { interface AttentionPageProps {
isOpenModal: boolean isOpenModal: boolean;
onCloseSpecialOffer?: () => void onCloseSpecialOffer?: () => void;
} }
function AttentionPage({ isOpenModal, onCloseSpecialOffer }: AttentionPageProps): JSX.Element { function AttentionPage({
const { t } = useTranslation() isOpenModal,
const navigate = useNavigate() onCloseSpecialOffer,
const handleNext = () => navigate(routes.client.feedback()) }: AttentionPageProps): JSX.Element {
const { t } = useTranslation();
// const onChangeCheckbox = (e: React.FormEvent<HTMLInputElement>) => { const navigate = useNavigate();
// if (e.currentTarget.checked) { const handleNext = () => navigate(routes.client.feedback());
// handleNext()
// }
// }
return ( return (
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
<SpecialWelcomeOffer open={isOpenModal} onClose={onCloseSpecialOffer} /> <SpecialWelcomeOffer open={isOpenModal} onClose={onCloseSpecialOffer} />
<img className={styles.icon} src="/stop-icon.png" alt="stop" /> <img className={styles.icon} src="/stop-icon.png" alt="stop" />
<Title variant='h2'>{t('aura.attention.title')}</Title> <Title variant="h2">{t("aura.attention.title")}</Title>
<p className={styles.text}>{t('aura.warming_up.body')}</p> <p className={styles.text}>{t("aura.warming_up.body")}</p>
<div className={styles['buttons-container']}> <div className={styles["buttons-container"]}>
{/* <CheckboxWithText text={t('not_ready_for_information')} onChange={onChangeCheckbox} /> */} <MainButton onClick={handleNext}>
{/* <Title variant='h2' className={styles.button} onClick={handleNext}>{t('aura.warming_up.button')}</Title> */} {t("aura.warmin_good.button")}
<MainButton onClick={handleNext}>{t('aura.warmin_good.button')}</MainButton> </MainButton>
<MainButton onClick={handleNext}>{t('aura.warmin_bad.button')}</MainButton> <MainButton onClick={handleNext}>
{t("aura.warmin_bad.button")}
</MainButton>
</div> </div>
</section> </section>
) );
} }
export default AttentionPage export default AttentionPage;

View File

@ -22,6 +22,7 @@
margin: 64px auto 0; margin: 64px auto 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
gap: 13px; gap: 13px;
} }

View 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;

View 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;
}

View 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;

View 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;
}

View 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;

View 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;
}

View 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;

View 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;
}

View File

@ -42,8 +42,6 @@ function CompatResultPage(): JSX.Element {
return navigate(routes.client.compatibility()); return navigate(routes.client.compatibility());
}; };
// const handleCompatibility = () => navigate(routes.client.compatibility());
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
const right_bday = const right_bday =
typeof rightUser.birthDate === "string" typeof rightUser.birthDate === "string"

View File

@ -30,7 +30,7 @@ const DatePicker: React.FC<DatePickerProps> = ({
}); });
const months = Array.from({ length: 36 }, (_, index) => 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) => const years = Array.from({ length: 81 }, (_, index) =>
(currentDate.getFullYear() - 80 + index).toString() (currentDate.getFullYear() - 80 + index).toString()
@ -60,6 +60,7 @@ const DatePicker: React.FC<DatePickerProps> = ({
} }
}, [selectedDate, onDateChange]); }, [selectedDate, onDateChange]);
return ( return (
<> <>
<div className={styles["date-picker-container"]}> <div className={styles["date-picker-container"]}>

View File

@ -48,8 +48,10 @@ const DatePickerItem: React.FC<DatePickerItemProps> = ({
}; };
useEffect(() => { useEffect(() => {
setTranslateY((data.indexOf(selectedValue) + (unit === "month" ? 12 : 0)) * -ITEM_HEIGHT) setTranslateY(
}, [selectedValue, data, unit]) (data.indexOf(selectedValue) + (unit === "month" ? 12 : 0)) * -ITEM_HEIGHT
);
}, [selectedValue, data, unit]);
useEffect(() => { useEffect(() => {
if (unit === "month") { 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 ( return (
<div className={styles["date-picker-viewport"]}> <div className={styles["date-picker-viewport"]}>
<div className={styles["date-picker-wheel"]}> <div className={styles["date-picker-wheel"]}>

View File

@ -176,9 +176,6 @@ function CompatibilityPage(): JSX.Element {
{!showNavbarFooter && ( {!showNavbarFooter && (
<div className={styles.cross} onClick={handleCross}></div> <div className={styles.cross} onClick={handleCross}></div>
)} )}
{/* <Title variant="h1" className={styles.title}>
{t("compatibility")}
</Title> */}
<div className={styles.content}> <div className={styles.content}>
{onboardingCompatibility && ( {onboardingCompatibility && (
<Title variant="h2" className={styles.iam}> <Title variant="h2" className={styles.iam}>

View File

@ -38,9 +38,6 @@ const calculateTop = (currentIdx: number, length: number, items: HTMLDivElement[
if (!item) return accumulator; if (!item) return accumulator;
return accumulator + item.clientHeight return accumulator + item.clientHeight
}, 1) + 8 * getMultiplier(currentIdx, length) }, 1) + 8 * getMultiplier(currentIdx, length)
// const itemHeight = 63
// return getMultiplier(currentIdx, length) * itemHeight1?.clientHeight
} }
function ProcessFlow({ items, onDone }: ProcessFlowProps): JSX.Element { function ProcessFlow({ items, onDone }: ProcessFlowProps): JSX.Element {

View File

@ -1,44 +1,38 @@
import { useNavigate } from 'react-router-dom' import { useNavigate } from "react-router-dom";
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next";
import MainButton from '../MainButton' import MainButton from "../MainButton";
import Title from '../Title' import Title from "../Title";
import routes from '@/routes' import routes from "@/routes";
import styles from './styles.module.css' import styles from "./styles.module.css";
import { useSelector } from 'react-redux' import { useSelector } from "react-redux";
import { selectors } from '@/store' import { selectors } from "@/store";
import { getZodiacSignByDate } from '@/services/zodiac-sign' import { getZodiacSignByDate } from "@/services/zodiac-sign";
// import SpecialWelcomeOffer from '../SpecialWelcomeOffer'
// import { useState } from 'react'
function DidYouKnowPage(): JSX.Element { function DidYouKnowPage(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation();
const navigate = useNavigate() const navigate = useNavigate();
const handleNext = () => navigate(routes.client.freePeriodInfo()) const handleNext = () => navigate(routes.client.freePeriodInfo());
// const [isOpenModal, setIsOpenModal] = useState(false) const birthdate = useSelector(selectors.selectBirthdate);
// const handleSpecialOffer = () => { const zodiacSign = getZodiacSignByDate(birthdate);
// setIsOpenModal(true)
// }
const birthdate = useSelector(selectors.selectBirthdate)
const zodiacSign = getZodiacSignByDate(birthdate)
return ( return (
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
{/* <SpecialWelcomeOffer open={isOpenModal} /> */}
<div className={styles.content}> <div className={styles.content}>
<Title variant='h1'>{t('did_you_know')}</Title> <Title variant="h1">{t("did_you_know")}</Title>
<p className={styles.zodiacInfo}> <p className={styles.zodiacInfo}>
{t('zodiac_sign_info', { zodiacSign })} {t("zodiac_sign_info", { zodiacSign })}
</p> </p>
</div> </div>
<footer className={styles.footer}> <footer className={styles.footer}>
<MainButton onClick={handleNext}> <MainButton onClick={handleNext}>
{t('learn_about_my_energy')} {t("learn_about_my_energy")}
</MainButton> </MainButton>
<span className={styles.skip} onClick={handleNext}>{t('skip_for_now')}</span> <span className={styles.skip} onClick={handleNext}>
{t("skip_for_now")}
</span>
</footer> </footer>
</section> </section>
) );
} }
export default DidYouKnowPage export default DidYouKnowPage;

View File

@ -12,7 +12,7 @@ function FeedbackPage(): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const api = useApi(); const api = useApi();
const handleNext = () => navigate(routes.client.emailEnter()); const handleNext = () => navigate(routes.client.auth());
const assetsData = useCallback(async () => { const assetsData = useCallback(async () => {
const { assets } = await api.getAssets({ category: String("au") }); const { assets } = await api.getAssets({ category: String("au") });

View File

@ -1,18 +1,10 @@
import { useCallback, useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { selectors } from "@/store"; import { selectors } from "@/store";
import { usePayment } from "@/payment"; import { usePayment } from "@/payment";
import { actions } from "@/store"; import { ApplePayBanner, GooglePayBanner } from "./methods";
import {
ApplePayBanner,
// ApplePayButton,
GooglePayBanner,
// GooglePayButton,
// CardButton,
// CardModal,
} from "./methods";
import ErrorModal from "./ErrorModal"; import ErrorModal from "./ErrorModal";
import UserHeader from "../UserHeader"; import UserHeader from "../UserHeader";
import Title from "../Title"; import Title from "../Title";
@ -21,33 +13,56 @@ import secure from "./secure.png";
import routes from "@/routes"; import routes from "@/routes";
import "./styles.css"; import "./styles.css";
import Header from "../Header"; 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 { function PaymentPage(): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
const { applePay } = usePayment(); const { applePay } = usePayment();
// const [openCardModal, setOpenCardModal] = useState(false); const api = useApi();
const [openStripeModal, setOpenStripeModal] = useState(false); const { token } = useAuth();
const [openErrorModal, setOpenErrorModal] = useState(false); const [openErrorModal, setOpenErrorModal] = useState(false);
const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const isLoading = applePay === null; const isLoading = applePay === null;
const isApplePayAvailable = const isApplePayAvailable =
import.meta.env.PROD && applePay?.canMakePayments(); import.meta.env.PROD && applePay?.canMakePayments();
const email = useSelector(selectors.selectEmail); const email = useSelector(selectors.selectEmail);
const isDiscount = useSelector(selectors.selectIsDiscount); const activeSubPlan = useSelector(selectors.selectActiveSubPlan);
const selectedPrice = useSelector(selectors.selectSelectedPrice);
const price = isDiscount const navigateToStripe = () => {
? (Math.round(selectedPrice || 0) / 2).toFixed(2) navigate(routes.client.paymentStripe());
: selectedPrice; };
const onSuccess = useCallback(() => {
dispatch(actions.status.update("subscribed")); const navigateToPayPal = async () => {
navigate(routes.client.wallpaper()); const { subscription_receipt } = await api.createSubscriptionReceipt({
}, [dispatch, navigate]); token,
const onError = useCallback((error: Error) => { itemInterval: "year",
console.error(error); way: "paypal",
setOpenErrorModal(true); 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 ( return (
<> <>
@ -68,33 +83,21 @@ function PaymentPage(): JSX.Element {
<Title variant="h1" className="mb-45"> <Title variant="h1" className="mb-45">
{t("choose_payment")} {t("choose_payment")}
</Title> </Title>
{/* {isApplePayAvailable ? ( <div className="payment-buttons-container">
<ApplePayButton onSuccess={onSuccess} onError={onError} /> <PayPalButton onClick={navigateToPayPal} />
) : ( <StripeButton onClick={navigateToStripe} />
<GooglePayButton onSuccess={onSuccess} onError={onError} /> </div>
)}
<div className="payment-divider">{t("or").toUpperCase()}</div>
<CardButton onClick={() => setOpenCardModal(true)} /> */}
<StripeButton onClick={() => setOpenStripeModal(true)} />
<p className="payment-warining"> <p className="payment-warining">
{t("will_be_charged", { {t("will_be_charged", {
strongText: ( strongText: (
<strong>{t("trial_price", { price: price })}</strong> <strong>
{t("trial_price", {
price: getPrice(activeSubPlan || null),
})}
</strong>
), ),
})} })}
</p> </p>
{/* <CardModal
open={openCardModal}
onClose={() => setOpenCardModal(false)}
onSuccess={onSuccess}
onError={onError}
/> */}
<StripeModal
open={openStripeModal}
onClose={() => setOpenStripeModal(false)}
onSuccess={onSuccess}
onError={onError}
/>
<ErrorModal <ErrorModal
open={openErrorModal} open={openErrorModal}
onClose={() => setOpenErrorModal(false)} onClose={() => setOpenErrorModal(false)}

View 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>
);
}

View File

@ -1,7 +1,6 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import MainButton from '@/components/MainButton' import MainButton from '@/components/MainButton'
// import card from './card.svg'
interface IStripeButtonProps { interface IStripeButtonProps {
onClick: () => void onClick: () => void
@ -11,7 +10,6 @@ export function StripeButton({ onClick }: IStripeButtonProps): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<MainButton color='blue' onClick={onClick}> <MainButton color='blue' onClick={onClick}>
{/* <img className='payment-card' src={card} alt='Credit / Debit Card' /> */}
{t('stripe')} {t('stripe')}
</MainButton> </MainButton>
) )

View File

@ -1,15 +1,18 @@
import MainButton from "@/components/MainButton"; import MainButton from "@/components/MainButton";
import Title from "@/components/Title"; import Title from "@/components/Title";
import { actions } from "@/store";
import { import {
PaymentElement, PaymentElement,
useElements, useElements,
useStripe, useStripe,
} from "@stripe/react-stripe-js"; } from "@stripe/react-stripe-js";
import { useState } from "react"; import { useState } from "react";
import { useDispatch } from "react-redux";
export default function CheckoutForm() { export default function CheckoutForm() {
const stripe = useStripe(); const stripe = useStripe();
const elements = useElements(); const elements = useElements();
const dispatch = useDispatch();
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const [isProcessing, setIsProcessing] = useState<boolean>(false); const [isProcessing, setIsProcessing] = useState<boolean>(false);
@ -27,18 +30,22 @@ export default function CheckoutForm() {
elements, elements,
confirmParams: { confirmParams: {
return_url: `https://${window.location.host}/payment/result`, return_url: `https://${window.location.host}/payment/result`,
} },
}); });
if (error) { if (error) {
setMessage(error?.message || "Oops! Something went wrong."); setMessage(error?.message || "Oops! Something went wrong.");
} }
dispatch(actions.status.update("subscribed"));
setIsProcessing(false); setIsProcessing(false);
}; };
return ( return (
<form id="payment-form" onSubmit={handleSubmit}> <form
className="payment-form-stripe"
id="payment-form"
onSubmit={handleSubmit}
>
<PaymentElement /> <PaymentElement />
<MainButton color="blue" disabled={isProcessing} id="submit"> <MainButton color="blue" disabled={isProcessing} id="submit">
<span id="button-text"> <span id="button-text">

View File

@ -6,6 +6,9 @@ import { Stripe, loadStripe } from "@stripe/stripe-js";
import { Elements } from "@stripe/react-stripe-js"; import { Elements } from "@stripe/react-stripe-js";
import CheckoutForm from "./CheckoutForm"; import CheckoutForm from "./CheckoutForm";
import { useAuth } from "@/auth"; import { useAuth } from "@/auth";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
import { PayPalReceiptPayload } from "@/api/resources/UserSubscriptionReceipts";
interface StripeModalProps { interface StripeModalProps {
open: boolean; open: boolean;
@ -22,10 +25,11 @@ export function StripeModal({
StripeModalProps): JSX.Element { StripeModalProps): JSX.Element {
const api = useApi(); const api = useApi();
const { token } = useAuth(); const { token } = useAuth();
const activeSubPlan = useSelector(selectors.selectActiveSubPlan);
const [stripePromise, setStripePromise] = const [stripePromise, setStripePromise] =
useState<Promise<Stripe | null> | null>(null); useState<Promise<Stripe | null> | null>(null);
const [clientSecret, setClientSecret] = useState<string>(""); const [clientSecret, setClientSecret] = useState<string>("");
const [isLoading, setIsLoading ] = useState(true); const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@ -37,11 +41,14 @@ StripeModalProps): JSX.Element {
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
(async () => { (async () => {
const { subscription_receipt } = await api.createSubscriptionReceipt({ const { subscription_receipt } = await api.createSubscriptionReceipt({
token, token,
way: "paypal",
itemInterval: "year", itemInterval: "year",
}); subscription_receipt: {
sub_plan_id: activeSubPlan?.id || "",
},
} as PayPalReceiptPayload);
const { client_secret } = subscription_receipt.data; const { client_secret } = subscription_receipt.data;
setClientSecret(client_secret); setClientSecret(client_secret);
setIsLoading(false); setIsLoading(false);

View File

@ -1,14 +1,20 @@
import { useNavigate } from 'react-router-dom' import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import routes from '@/routes' import routes from "@/routes";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import Title from "@/components/Title"; import Title from "@/components/Title";
import MainButton from "@/components/MainButton"; import MainButton from "@/components/MainButton";
import { useDispatch } from "react-redux";
import { actions } from "@/store";
function PaymentSuccessPage(): JSX.Element { function PaymentSuccessPage(): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate() const navigate = useNavigate();
const handleNext = () => navigate(routes.client.home()) const dispatch = useDispatch();
const handleNext = () => {
dispatch(actions.status.update("subscribed"));
navigate(routes.client.home());
};
return ( return (
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
@ -17,7 +23,9 @@ function PaymentSuccessPage(): JSX.Element {
<Title variant="h1">{t("auweb.pay_good.title")}</Title> <Title variant="h1">{t("auweb.pay_good.title")}</Title>
<p>{t("auweb.pay_good.text1")}</p> <p>{t("auweb.pay_good.text1")}</p>
</div> </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> </section>
); );
} }

View File

@ -2,18 +2,22 @@ import { useNavigate } from "react-router-dom";
import routes from "@/routes"; import routes from "@/routes";
import { useEffect } from "react"; import { useEffect } from "react";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import { useDispatch } from "react-redux";
import { actions } from "@/store";
function PaymentResultPage(): JSX.Element { function PaymentResultPage(): JSX.Element {
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useDispatch();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const status = searchParams.get("redirect_status"); const status = searchParams.get("redirect_status");
useEffect(() => { useEffect(() => {
if (status === "succeeded") { if (status === "succeeded") {
dispatch(actions.status.update("subscribed"));
return navigate(routes.client.paymentSuccess()); return navigate(routes.client.paymentSuccess());
} }
return navigate(routes.client.paymentFail()); return navigate(routes.client.paymentFail());
}, [navigate, status]); }, [navigate, status, dispatch]);
return <></>; return <></>;
} }

View File

@ -41,7 +41,7 @@
.payment-inforamtion { .payment-inforamtion {
font-size: 12px; font-size: 12px;
line-height: 1.5; line-height: 1.5;
letter-spacing: .0008em; letter-spacing: 0.0008em;
} }
.payment-chargebee { .payment-chargebee {
@ -82,6 +82,13 @@
margin-right: 12px; margin-right: 12px;
} }
.payment-form-stripe {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.payment-loader { .payment-loader {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -100,6 +107,14 @@
font-weight: 500; font-weight: 500;
} }
.payment-buttons-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.pay-btn, .pay-btn,
.gpay-button-fake-loader, .gpay-button-fake-loader,
.apple-pay-button-placeholder { .apple-pay-button-placeholder {
@ -138,6 +153,6 @@
display: flex; display: flex;
align-items: end; align-items: end;
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: rgba(0,0,0,.5); background-color: rgba(0, 0, 0, 0.5);
padding: 12px 15% 10px; padding: 12px 15% 10px;
} }

View File

@ -1,23 +1,27 @@
import './styles.css' import "./styles.css";
interface PolicyProps { interface PolicyProps {
children: string children: string | JSX.Element | null;
sizing?: 'small' | 'medium' | 'large' sizing?: "small" | "medium" | "large";
className?: string className?: string;
} }
const sizes = { const sizes = {
small: 'policy--small', small: "policy--small",
medium: 'policy--medium', medium: "policy--medium",
large: 'policy--large', large: "policy--large",
} };
function Policy({ children, sizing = 'small', className = '' }: PolicyProps): JSX.Element { function Policy({
children,
sizing = "small",
className = "",
}: PolicyProps): JSX.Element {
return ( return (
<div className={`policy ${sizes[sizing]} ${className}`}> <div className={`policy ${sizes[sizing]} ${className}`}>
<p>{children}</p> <p>{children}</p>
</div> </div>
) );
} }
export default Policy export default Policy;

View File

@ -1,29 +1,17 @@
import { removeAfterDot, roundToWhole } from "@/services/price";
import { Currency, Locale, Price } from "../PaymentTable"; import { Currency, Locale, Price } from "../PaymentTable";
import { IPrice } from "../PriceList";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
const currency = Currency.USD; const currency = Currency.USD;
const locale = Locale.EN; 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 { interface PriceItemProps {
id: string,
value: number,
active: boolean; active: boolean;
click: (id: number) => void; click: (id: string) => void;
} }
function PriceItem({ function PriceItem({
@ -31,11 +19,11 @@ function PriceItem({
value, value,
active, active,
click, click,
}: IPrice & PriceItemProps): JSX.Element { }: PriceItemProps): JSX.Element {
const _price = new Price(roundToWhole(value), currency, locale); const _price = new Price(roundToWhole(value), currency, locale);
const compatClassName = () => { const compatClassName = () => {
const isPopular = id === 3; const isPopular = id === 'stripe.7';
const isActive = active; const isActive = active;
return `${styles.container} ${isPopular ? styles.popular : ""} ${isActive ? styles.active : ""}`; return `${styles.container} ${isPopular ? styles.popular : ""} ${isActive ? styles.active : ""}`;
}; };

View File

@ -1,64 +1,53 @@
import { useState } from 'react' import { useState } from "react";
import PriceItem from '../PriceItem' import PriceItem from "../PriceItem";
import styles from './styles.module.css' import styles from "./styles.module.css";
import { useDispatch } from 'react-redux' import { useDispatch } from "react-redux";
import { actions } from '@/store' import { actions } from "@/store";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
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
},
]
interface PriceListProps { interface PriceListProps {
activeItem: number | null subPlans: ISubscriptionPlan[];
click: () => void activeItem: number | null;
click: () => void;
} }
function PriceList({click}: PriceListProps): JSX.Element { const getPrice = (plan: ISubscriptionPlan) => {
const dispatch = useDispatch(); return (plan.trial?.price_cents || 0) / 100;
const [activePriceItem, setActivePriceItem] = useState<number | null>(null) };
const priceItemClick = (id: number) => { function PriceList({ click, subPlans }: PriceListProps): JSX.Element {
setActivePriceItem(id) const dispatch = useDispatch();
const activePriceItem = prices.find((item) => item.id === Number(id)) const [activePlanItem, setActivePlanItem] =
if (activePriceItem) { useState<ISubscriptionPlan | null>(null);
const priceItemClick = (id: string) => {
const activePlan = subPlans.find((item) => item.id === String(id)) || null;
setActivePlanItem(activePlan);
if (activePlan) {
dispatch( dispatch(
actions.payment.update({ actions.payment.update({
selectedPrice: activePriceItem.value activeSubPlan: activePlan,
}) })
); );
} }
setTimeout(() => { setTimeout(() => {
click() click();
}, 1000) }, 1000);
} };
return ( return (
<div className={`${styles.container}`}> <div className={`${styles.container}`}>
{prices.map((price, idx) => ( {subPlans.map((plan, idx) => (
<PriceItem active={price.id === activePriceItem} key={idx} value={price.value} id={price.id} click={priceItemClick} /> <PriceItem
active={plan.id === activePlanItem?.id}
key={idx}
value={getPrice(plan)}
id={plan.id}
click={priceItemClick}
/>
))} ))}
</div> </div>
) );
} }
export default PriceList export default PriceList;

View File

@ -1,46 +1,87 @@
import { useNavigate } from 'react-router-dom' import { useNavigate } from "react-router-dom";
import routes from '@/routes' import routes from "@/routes";
import styles from './styles.module.css' import styles from "./styles.module.css";
import UserHeader from '../UserHeader' import UserHeader from "../UserHeader";
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from '@/store' import { actions, selectors } from "@/store";
import Title from '../Title' import Title from "../Title";
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next";
import EmailsList from '../EmailsList' import EmailsList from "../EmailsList";
import PriceList from '../PriceList' 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 { function PriceListPage(): JSX.Element {
const { t } = useTranslation() const { t, i18n } = useTranslation();
const navigate = useNavigate() const locale = i18n.language;
const api = useApi();
const navigate = useNavigate();
const dispatch = useDispatch(); const dispatch = useDispatch();
const homeConfig = useSelector(selectors.selectHome); const homeConfig = useSelector(selectors.selectHome);
const selectedPrice = useSelector(selectors.selectSelectedPrice) 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 email = useSelector(selectors.selectEmail)
const handleNext = () => { const handleNext = () => {
dispatch( dispatch(
actions.siteConfig.update({ actions.siteConfig.update({
home: { pathFromHome: homeConfig.pathFromHome, isShowNavbar: false }, home: { pathFromHome: homeConfig.pathFromHome, isShowNavbar: false },
}) })
); );
navigate(routes.client.subscription()) navigate(routes.client.subscription());
} };
return ( return (
<> <>
<UserHeader email={email} /> <UserHeader email={email} />
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
<Title className={styles.title} variant='h2'>{t('choose_your_own_fee')}</Title> {!!subPlans.length && (
<p className={styles.slogan}>{t('aura.web.price_selection')}</p> <>
<div className={styles['emails-list-container']}> <Title className={styles.title} variant="h2">
<EmailsList /> {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>
<div className={styles['price-list-container']}> <div className={styles["price-list-container"]}>
<PriceList activeItem={selectedPrice} click={handleNext} /> <PriceList
activeItem={selectedPrice}
subPlans={subPlans}
click={handleNext}
/>
</div> </div>
</section> </>
)}
{!subPlans.length && <Loader />}
</section>
</> </>
) );
} }
export default PriceListPage export default PriceListPage;

View File

@ -33,6 +33,10 @@ function SpecialWelcomeOffer({ open, onClose }: ModalTopProps): JSX.Element {
navigate(routes.client.paymentMethod()); navigate(routes.client.paymentMethod());
}; };
const handleMoreAbout = () => {
window.location.href = "https://witapps.us/en/aura";
};
return ( return (
<> <>
{open ? ( {open ? (
@ -69,9 +73,7 @@ function SpecialWelcomeOffer({ open, onClose }: ModalTopProps): JSX.Element {
<MainButton <MainButton
// disabled // disabled
className={styles["button-black"]} className={styles["button-black"]}
onClick={() => { onClick={handleMoreAbout}
console.log("click");
}}
> >
<img className={styles["button-icon"]} src="/leo.png" alt="Leo" /> <img className={styles["button-icon"]} src="/leo.png" alt="Leo" />
{t("au.more_llc.button")} {t("au.more_llc.button")}

View 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>
);
}

View 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;
}

View File

@ -13,28 +13,34 @@ import styles from "./styles.module.css";
import Header from "../Header"; import Header from "../Header";
import SpecialWelcomeOffer from "../SpecialWelcomeOffer"; import SpecialWelcomeOffer from "../SpecialWelcomeOffer";
import { useState } from "react"; import { useState } from "react";
import { ITrial } from "@/api/resources/SubscriptionPlans";
const currency = Currency.USD; const currency = Currency.USD;
const locale = Locale.EN; 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 { function SubscriptionPage(): JSX.Element {
const [isOpenModal, setIsOpenModal] = useState(false); const [isOpenModal, setIsOpenModal] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const activeSubPlan = useSelector(selectors.selectActiveSubPlan);
const paymentItems = [ const paymentItems = [
{ {
title: "Per 7-Day Trial For", title: activeSubPlan?.name || "Per 7-Day Trial For",
price: 1.0, price: getPriceFromTrial(activeSubPlan?.trial || null),
description: t("au.2week_plan.web"), description: activeSubPlan?.desc.length
? activeSubPlan?.desc
: t("au.2week_plan.web"),
}, },
]; ];
const navigate = useNavigate(); const navigate = useNavigate();
const email = useSelector(selectors.selectEmail); 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 handleClick = () => navigate(routes.client.paymentMethod());
const handleCross = () => setIsOpenModal(true); const handleCross = () => setIsOpenModal(true);
const policyLink = ( const policyLink = (
@ -42,13 +48,13 @@ function SubscriptionPage(): JSX.Element {
{t("subscription_policy")} {t("subscription_policy")}
</a> </a>
); );
console.log({ itemPrice });
return ( return (
<> <>
<SpecialWelcomeOffer open={isOpenModal} onClose={handleClick} /> <SpecialWelcomeOffer open={isOpenModal} onClose={handleClick} />
<Header classCross={styles.cross} clickCross={handleCross} /> <Header classCross={styles.cross} clickCross={handleCross} />
<UserHeader email={email} /> <UserHeader email={email} />
<section className="page"> <section className={`${styles.page} page`}>
<CallToAction /> <CallToAction />
<Countdown start={10} /> <Countdown start={10} />
<PaymentTable <PaymentTable
@ -59,7 +65,12 @@ function SubscriptionPage(): JSX.Element {
<div className={styles["subscription-action"]}> <div className={styles["subscription-action"]}>
<MainButton onClick={handleClick}>{t("get_access")}</MainButton> <MainButton onClick={handleClick}>{t("get_access")}</MainButton>
</div> </div>
<Policy>{t("subscription_text", { policyLink })}</Policy> <Policy>
<>
{t("auweb.agree.text1")}
{t("subscription_text", { policyLink })}
</>
</Policy>
</section> </section>
</> </>
); );

View File

@ -1,3 +1,7 @@
.page {
padding-bottom: 32px !important;
}
.subscription-action { .subscription-action {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
@ -5,7 +9,7 @@
right: 0; right: 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
background-color: #fff; background-color: transparent;
padding: 15px; padding: 15px;
} }

View File

@ -47,7 +47,9 @@ function getZodiacParagraphs(
{paragraph.title} {paragraph.title}
</Headline> </Headline>
{getTypeOfContent(paragraph.content) === "string" {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)} : getZodiacParagraphs(paragraph.content as ZodiacParagraph[], depth)}
</div> </div>
); );
@ -58,7 +60,7 @@ function WallpaperPage(): JSX.Element {
const api = useApi(); const api = useApi();
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const locale = i18n.language; const locale = i18n.language;
const token = useSelector(selectors.selectToken) const token = useSelector(selectors.selectToken);
const { const {
user, user,
@ -111,20 +113,6 @@ function WallpaperPage(): JSX.Element {
</div> </div>
))} ))}
</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"]}> <div className={styles["wallpaper-forecast"]}>
{getZodiacParagraphs(zodiacInfo?.paragraphs || [])} {getZodiacParagraphs(zodiacInfo?.paragraphs || [])}
</div> </div>

View File

@ -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.", 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.", purposes: "For entertaiment purposes only.",
get_access: "Get access", 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", subscription_policy: "Subscription policy",
company_name: "Wit Apps LLC, California, US", company_name: "Wit Apps LLC, California, US",
choose_payment: "Choose Payment Method", choose_payment: "Choose Payment Method",
@ -81,6 +81,7 @@ export default {
you_and: "You and <user>", you_and: "You and <user>",
sign: "Sign", sign: "Sign",
stripe: "Stripe", stripe: "Stripe",
payPal: "PayPal",
'aura-10_breath-button': "Increase up to 10%. Practice for the Energy of Money", '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", '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...", "breathe-subtitle": "Breathing practice will help improve your aura. Breath in the positive energy, breathe out the negative...",

View File

@ -1,7 +1,8 @@
import type { UserStatus } from "./types"; import type { UserStatus } from "./types";
const host = ""; 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 prefix = "api/v1";
const routes = { const routes = {
@ -12,6 +13,8 @@ const routes = {
freePeriodInfo: () => [host, "free-period"].join("/"), freePeriodInfo: () => [host, "free-period"].join("/"),
birthtime: () => [host, "birthtime"].join("/"), birthtime: () => [host, "birthtime"].join("/"),
emailEnter: () => [host, "email"].join("/"), 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("/"), createProfile: () => [host, "profile", "create"].join("/"),
attention: () => [host, "attention"].join("/"), attention: () => [host, "attention"].join("/"),
@ -20,6 +23,7 @@ const routes = {
paymentResult: () => [host, "payment", "result"].join("/"), paymentResult: () => [host, "payment", "result"].join("/"),
paymentSuccess: () => [host, "payment", "success"].join("/"), paymentSuccess: () => [host, "payment", "success"].join("/"),
paymentFail: () => [host, "payment", "fail"].join("/"), paymentFail: () => [host, "payment", "fail"].join("/"),
paymentStripe: () => [host, "payment", "stripe"].join("/"),
wallpaper: () => [host, "wallpaper"].join("/"), wallpaper: () => [host, "wallpaper"].join("/"),
static: () => [host, "static", ":typeId"].join("/"), static: () => [host, "static", ":typeId"].join("/"),
legal: (type: string) => [host, "static", type].join("/"), legal: (type: string) => [host, "static", type].join("/"),
@ -31,10 +35,13 @@ const routes = {
breathResult: () => [host, "breath", "result"].join("/"), breathResult: () => [host, "breath", "result"].join("/"),
}, },
server: { 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("/"), user: () => [apiHost, prefix, "user.json"].join("/"),
token: () => [apiHost, prefix, "auth", "token.json"].join("/"), token: () => [apiHost, prefix, "auth", "token.json"].join("/"),
elements: () => [apiHost, prefix, "elements.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) => element: (type: string) =>
[apiHost, prefix, "elements", `${type}.json`].join("/"), [apiHost, prefix, "elements", `${type}.json`].join("/"),
apps: (bundleId: string) => apps: (bundleId: string) =>
@ -50,6 +57,7 @@ const routes = {
[apiHost, prefix, "user", "payment_intents.json"].join("/"), [apiHost, prefix, "user", "payment_intents.json"].join("/"),
subscriptionItems: () => subscriptionItems: () =>
[apiHost, prefix, "user", "subscription", "item_prices.json"].join("/"), [apiHost, prefix, "user", "subscription", "item_prices.json"].join("/"),
subscriptionPlans: () => [apiHost, prefix, "sub_plans.json"].join("/"),
subscriptionCheckout: () => subscriptionCheckout: () =>
[apiHost, prefix, "user", "subscription", "checkout", "new.json"].join( [apiHost, prefix, "user", "subscription", "checkout", "new.json"].join(
"/" "/"
@ -71,7 +79,7 @@ const routes = {
[apiHost, prefix, "user", "callbacks.json"].join("/"), [apiHost, prefix, "user", "callbacks.json"].join("/"),
getUserCallbacks: (id: string) => getUserCallbacks: (id: string) =>
[apiHost, prefix, "user", "callbacks", `${id}.json`].join("/"), [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 = [ export const withCrossButtonRoutes = [
// routes.client.attention(), // routes.client.attention(),
routes.client.subscription(), routes.client.subscription(),
routes.client.paymentMethod() routes.client.paymentMethod(),
]; ];
export const hasCrossButton = (path: string) => export const hasCrossButton = (path: string) =>
withCrossButtonRoutes.includes(path); withCrossButtonRoutes.includes(path);
@ -121,6 +129,7 @@ export const withoutFooterRoutes = [
routes.client.paymentResult(), routes.client.paymentResult(),
routes.client.paymentSuccess(), routes.client.paymentSuccess(),
routes.client.paymentFail(), routes.client.paymentFail(),
routes.client.paymentStripe(),
]; ];
export const hasNoFooter = (path: string) => export const hasNoFooter = (path: string) =>
!withoutFooterRoutes.includes(path); !withoutFooterRoutes.includes(path);
@ -155,7 +164,7 @@ export const getRouteBy = (status: UserStatus): string => {
case "unsubscribed": case "unsubscribed":
return routes.client.subscription(); return routes.client.subscription();
case "subscribed": case "subscribed":
return routes.client.wallpaper(); return routes.client.home();
default: default:
throw new Error(`Unknown user status, received status is "${status}"`); throw new Error(`Unknown user status, received status is "${status}"`);
} }

View 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];
};

View File

@ -24,6 +24,7 @@ import onboardingConfig, {
} from "./onboarding"; } from "./onboarding";
import payment, { import payment, {
actions as paymentActions, actions as paymentActions,
selectActiveSubPlan,
selectIsDiscount, selectIsDiscount,
} from "./payment"; } from "./payment";
import subscriptionPlans, { import subscriptionPlans, {
@ -71,6 +72,7 @@ export const selectors = {
selectSelfName, selectSelfName,
selectCategoryId, selectCategoryId,
selectSelectedPrice, selectSelectedPrice,
selectActiveSubPlan,
selectUserCallbacksDescription, selectUserCallbacksDescription,
selectUserCallbacksPrevStat, selectUserCallbacksPrevStat,
selectHome, selectHome,

View File

@ -1,14 +1,17 @@
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import { createSlice, createSelector } from "@reduxjs/toolkit"; import { createSlice, createSelector } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit";
interface IPayment { interface IPayment {
selectedPrice: number | null; selectedPrice: number | null;
isDiscount: boolean; isDiscount: boolean;
activeSubPlan: ISubscriptionPlan | null;
} }
const initialState: IPayment = { const initialState: IPayment = {
selectedPrice: null, selectedPrice: null,
isDiscount: false, isDiscount: false,
activeSubPlan: null
}; };
const paymentSlice = createSlice({ const paymentSlice = createSlice({
@ -27,6 +30,10 @@ export const selectSelectedPrice = createSelector(
(state: { payment: IPayment }) => state.payment.selectedPrice, (state: { payment: IPayment }) => state.payment.selectedPrice,
(payment) => payment (payment) => payment
); );
export const selectActiveSubPlan = createSelector(
(state: { payment: IPayment }) => state.payment.activeSubPlan,
(payment) => payment
);
export const selectIsDiscount = createSelector( export const selectIsDiscount = createSelector(
(state: { payment: IPayment }) => state.payment.isDiscount, (state: { payment: IPayment }) => state.payment.isDiscount,
(payment) => payment (payment) => payment

View File

@ -1,8 +1,8 @@
import { Chargebee } from '@chargebee/chargebee-js-types' import { Chargebee } from "@chargebee/chargebee-js-types";
declare global { declare global {
interface Window { interface Window {
Chargebee: typeof Chargebee Chargebee: typeof Chargebee;
} }
} }
@ -14,19 +14,19 @@ export enum EDirectionOnboarding {
} }
export interface FormField<T> { export interface FormField<T> {
name: string name: string;
value: T value: T;
label?: string | null label?: string | null;
placeholder?: string | null placeholder?: string | null;
inputClassName?: string inputClassName?: string;
onValid: (value: string) => void onValid: (value: string) => void;
onInvalid: () => void onInvalid: () => void;
} }
export interface SignupForm { export interface SignupForm {
email: string email: string;
birthdate: string birthdate: string;
birthtime: string birthtime: string;
} }
export type UserStatus = 'lead' | 'registred' | 'subscribed' | 'unsubscribed' export type UserStatus = "lead" | "registred" | "subscribed" | "unsubscribed";