diff --git a/public/apple-auth-icon.png b/public/apple-auth-icon.png new file mode 100644 index 0000000..158c7ef Binary files /dev/null and b/public/apple-auth-icon.png differ diff --git a/public/apple-auth-icon.svg b/public/apple-auth-icon.svg new file mode 100644 index 0000000..c388218 --- /dev/null +++ b/public/apple-auth-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/google-auth-icon.png b/public/google-auth-icon.png new file mode 100644 index 0000000..4136c8c Binary files /dev/null and b/public/google-auth-icon.png differ diff --git a/public/google-auth-icon.svg b/public/google-auth-icon.svg new file mode 100644 index 0000000..9f4a086 --- /dev/null +++ b/public/google-auth-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/padlock_icon_animation_closing.png b/public/padlock_icon_animation_closing.png new file mode 100644 index 0000000..06ec262 Binary files /dev/null and b/public/padlock_icon_animation_closing.png differ diff --git a/src/api/api.ts b/src/api/api.ts index 7655863..f6c0744 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -19,11 +19,16 @@ import { AIRequests, UserCallbacks, Translations, - Zodiacs + Zodiacs, + GoogleAuth, + SubscriptionPlans, + AppleAuth } from './resources' const api = { auth: createMethod(AuthTokens.createRequest), + appleAuth: createMethod(AppleAuth.createRequest), + googleAuth: createMethod(GoogleAuth.createRequest), getAppConfig: createMethod(Apps.createRequest), getElement: createMethod(Element.createRequest), getElements: createMethod(Elements.createRequest), @@ -34,6 +39,7 @@ const api = { getDailyForecasts: createMethod(DailyForecasts.createRequest), getAuras: createMethod(Auras.createRequest), getSubscriptionItems: createMethod(SubscriptionItems.createRequest), + getSubscriptionPlans: createMethod(SubscriptionPlans.createRequest), getSubscriptionCheckout: createMethod(SubscriptionCheckout.createRequest), getSubscriptionStatus: createMethod(SubscriptionStatus.createRequest), getSubscriptionReceipt: createMethod(SubscriptionReceipts.createGetRequest), diff --git a/src/api/resources/AIRequests.ts b/src/api/resources/AIRequests.ts index dfe1ca8..6f796e2 100644 --- a/src/api/resources/AIRequests.ts +++ b/src/api/resources/AIRequests.ts @@ -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) }); }; diff --git a/src/api/resources/AppleAuth.ts b/src/api/resources/AppleAuth.ts new file mode 100644 index 0000000..da00711 --- /dev/null +++ b/src/api/resources/AppleAuth.ts @@ -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" }); +}; diff --git a/src/api/resources/AssetCategories.ts b/src/api/resources/AssetCategories.ts index 60f1c01..a8bf1d7 100644 --- a/src/api/resources/AssetCategories.ts +++ b/src/api/resources/AssetCategories.ts @@ -1,6 +1,4 @@ import routes from "@/routes" -// import { AuthPayload } from "../types" -// import { getAuthHeaders } from "../utils" export interface Payload { locale: string diff --git a/src/api/resources/Assets.ts b/src/api/resources/Assets.ts index 2af5a00..0036e41 100644 --- a/src/api/resources/Assets.ts +++ b/src/api/resources/Assets.ts @@ -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 diff --git a/src/api/resources/GoogleAuth.ts b/src/api/resources/GoogleAuth.ts new file mode 100644 index 0000000..4f66cee --- /dev/null +++ b/src/api/resources/GoogleAuth.ts @@ -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", + }); +}; diff --git a/src/api/resources/SubscriptionPlans.ts b/src/api/resources/SubscriptionPlans.ts new file mode 100644 index 0000000..3e91f12 --- /dev/null +++ b/src/api/resources/SubscriptionPlans.ts @@ -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" }); +}; diff --git a/src/api/resources/User.ts b/src/api/resources/User.ts index 10df905..2bab6eb 100644 --- a/src/api/resources/User.ts +++ b/src/api/resources/User.ts @@ -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 + user: Partial; } export interface Response { - user: User + user: User; meta?: { links: { - self: string - } - } + self: string; + }; + }; } export interface UserPatch { - locale: string - timezone: string - profile_attributes: Partial & { - 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, + }); +}; diff --git a/src/api/resources/UserSubscriptionReceipts.ts b/src/api/resources/UserSubscriptionReceipts.ts index 3fc8138..0692ff2 100644 --- a/src/api/resources/UserSubscriptionReceipts.ts +++ b/src/api/resources/UserSubscriptionReceipts.ts @@ -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, }, }; } diff --git a/src/api/resources/index.ts b/src/api/resources/index.ts index 00e1ec1..a679cc4 100644 --- a/src/api/resources/index.ts +++ b/src/api/resources/index.ts @@ -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"; diff --git a/src/auth/useAuth.ts b/src/auth/useAuth.ts index f002396..8cc4eea 100644 --- a/src/auth/useAuth.ts +++ b/src/auth/useAuth.ts @@ -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); diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 5698856..7cd075d 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -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(false); const [leoApng, setLeoApng] = useState(Error); + const [padLockApng, setPadLockApng] = useState(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(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 ( }> - } /> - } /> - } /> - } - /> - - } - /> - } /> - } /> - } /> - } /> - } /> - } - /> - } - /> - } - /> - } /> - } /> - } - /> - } - /> - } /> - } /> - } /> - } /> - }> + } /> + } /> + } + /> + } + /> + + } + /> + } /> + } /> + } /> + } + /> + } + /> + } + /> + } /> + } /> + {/* } - /> - }> + /> */} + + } + /> + }> + }> + } + /> + } + /> + } + /> + } + /> + } + /> + + }> + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + + } /> @@ -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" ? ( + + ) : ( + + ); +} + 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" ? ( + + ) : ( + + ); +} + function SkipStep(): JSX.Element { const { user } = useAuth(); return user ? ( @@ -271,14 +361,4 @@ function MainPage(): JSX.Element { return ; } -function ProtectWallpaperPage(): JSX.Element { - const status = useSelector(selectors.selectStatus); - return ; - return status === "subscribed" ? ( - - ) : ( - - ); -} - export default App; diff --git a/src/components/AttentionPage/index.tsx b/src/components/AttentionPage/index.tsx index 1b69440..783d581 100644 --- a/src/components/AttentionPage/index.tsx +++ b/src/components/AttentionPage/index.tsx @@ -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) => { - // 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 (
stop - {t('aura.attention.title')} -

{t('aura.warming_up.body')}

-
- {/* */} - {/* {t('aura.warming_up.button')} */} - {t('aura.warmin_good.button')} - {t('aura.warmin_bad.button')} + {t("aura.attention.title")} +

{t("aura.warming_up.body")}

+
+ + {t("aura.warmin_good.button")} + + + {t("aura.warmin_bad.button")} +
- ) + ); } -export default AttentionPage +export default AttentionPage; diff --git a/src/components/AttentionPage/styles.module.css b/src/components/AttentionPage/styles.module.css index 4185856..7e997cf 100644 --- a/src/components/AttentionPage/styles.module.css +++ b/src/components/AttentionPage/styles.module.css @@ -22,6 +22,7 @@ margin: 64px auto 0; display: flex; flex-direction: column; + align-items: center; gap: 13px; } diff --git a/src/components/AuthPage/AppleAuthButton/index.tsx b/src/components/AuthPage/AppleAuthButton/index.tsx new file mode 100644 index 0000000..733340d --- /dev/null +++ b/src/components/AuthPage/AppleAuthButton/index.tsx @@ -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 ( + + Apple + {"Sign in with Apple"} + + ); +} + +export default AppleAuthButton; diff --git a/src/components/AuthPage/AppleAuthButton/styles.module.css b/src/components/AuthPage/AppleAuthButton/styles.module.css new file mode 100644 index 0000000..ab68165 --- /dev/null +++ b/src/components/AuthPage/AppleAuthButton/styles.module.css @@ -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; +} diff --git a/src/components/AuthPage/GoogleAuthButton/index.tsx b/src/components/AuthPage/GoogleAuthButton/index.tsx new file mode 100644 index 0000000..d5ab7f4 --- /dev/null +++ b/src/components/AuthPage/GoogleAuthButton/index.tsx @@ -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 ( + + Google + {"Sign in with Google"} + + ); +} + +export default AppleAuthButton; diff --git a/src/components/AuthPage/GoogleAuthButton/styles.module.css b/src/components/AuthPage/GoogleAuthButton/styles.module.css new file mode 100644 index 0000000..6cb39f3 --- /dev/null +++ b/src/components/AuthPage/GoogleAuthButton/styles.module.css @@ -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; +} diff --git a/src/components/AuthPage/index.tsx b/src/components/AuthPage/index.tsx new file mode 100644 index 0000000..7678877 --- /dev/null +++ b/src/components/AuthPage/index.tsx @@ -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(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 ( +
+ + Sign in to save your energy analysis, horoscope, and predictions. + + +

{t("we_dont_share")}

+
+ + +
+ + {t("_continue_agree", { + eulaLink: ( + + {t("eula")} + + ), + privacyLink: ( + + {t("privacy_policy")} + + ), + })} + +
+ ); +} + +export default AuthPage; diff --git a/src/components/AuthPage/styles.module.css b/src/components/AuthPage/styles.module.css new file mode 100644 index 0000000..649f7c0 --- /dev/null +++ b/src/components/AuthPage/styles.module.css @@ -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; +} \ No newline at end of file diff --git a/src/components/AuthResultPage/index.tsx b/src/components/AuthResultPage/index.tsx new file mode 100644 index 0000000..670ce39 --- /dev/null +++ b/src/components/AuthResultPage/index.tsx @@ -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(null); + const [error, setError] = useState(false); + const [isLoading, setIsLoading] = useState(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 ( +
+ {isLoading && } + {(error || apiError) && ( + <> + Something went wrong + Try again + + )} + {apiError && ( + + )} + {!apiError && !error && !isLoading && access_token.length && ( + Success Icon + )} +
+ ); +} + +export default AuthResultPage; diff --git a/src/components/AuthResultPage/styles.module.css b/src/components/AuthResultPage/styles.module.css new file mode 100644 index 0000000..9757c27 --- /dev/null +++ b/src/components/AuthResultPage/styles.module.css @@ -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; +} diff --git a/src/components/CompatResultPage/index.tsx b/src/components/CompatResultPage/index.tsx index 16a737a..bc07024 100644 --- a/src/components/CompatResultPage/index.tsx +++ b/src/components/CompatResultPage/index.tsx @@ -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" diff --git a/src/components/Compatibility/DatePicker.tsx b/src/components/Compatibility/DatePicker.tsx index 2ce5043..c399f97 100644 --- a/src/components/Compatibility/DatePicker.tsx +++ b/src/components/Compatibility/DatePicker.tsx @@ -30,7 +30,7 @@ const DatePicker: React.FC = ({ }); 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 = ({ } }, [selectedDate, onDateChange]); + return ( <>
diff --git a/src/components/Compatibility/DatePickerItem.tsx b/src/components/Compatibility/DatePickerItem.tsx index 9aafa61..6330e70 100644 --- a/src/components/Compatibility/DatePickerItem.tsx +++ b/src/components/Compatibility/DatePickerItem.tsx @@ -48,8 +48,10 @@ const DatePickerItem: React.FC = ({ }; 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 = ({ } }; - // const handleMouseDown = (event: React.MouseEvent) => { - // 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 (
diff --git a/src/components/Compatibility/index.tsx b/src/components/Compatibility/index.tsx index a110b52..9ef9e3e 100644 --- a/src/components/Compatibility/index.tsx +++ b/src/components/Compatibility/index.tsx @@ -176,9 +176,6 @@ function CompatibilityPage(): JSX.Element { {!showNavbarFooter && (
)} - {/* - {t("compatibility")} - */}
{onboardingCompatibility && ( diff --git a/src/components/CreateProfilePage/ProcessFlow.tsx b/src/components/CreateProfilePage/ProcessFlow.tsx index 41f0e4a..60bdd34 100644 --- a/src/components/CreateProfilePage/ProcessFlow.tsx +++ b/src/components/CreateProfilePage/ProcessFlow.tsx @@ -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 { diff --git a/src/components/DidYouKnowPage/index.tsx b/src/components/DidYouKnowPage/index.tsx index 23aedb4..f6da302 100644 --- a/src/components/DidYouKnowPage/index.tsx +++ b/src/components/DidYouKnowPage/index.tsx @@ -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')} + {t("did_you_know")}

- {t('zodiac_sign_info', { zodiacSign })} + {t("zodiac_sign_info", { zodiacSign })}

- {t('learn_about_my_energy')} + {t("learn_about_my_energy")} - {t('skip_for_now')} + + {t("skip_for_now")} +
- ) + ); } -export default DidYouKnowPage +export default DidYouKnowPage; diff --git a/src/components/FeedbackPage/index.tsx b/src/components/FeedbackPage/index.tsx index 024c450..3b26183 100644 --- a/src/components/FeedbackPage/index.tsx +++ b/src/components/FeedbackPage/index.tsx @@ -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") }); diff --git a/src/components/PaymentPage/index.tsx b/src/components/PaymentPage/index.tsx index c3f40e1..bd146cd 100644 --- a/src/components/PaymentPage/index.tsx +++ b/src/components/PaymentPage/index.tsx @@ -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 { {t("choose_payment")} - {/* {isApplePayAvailable ? ( - - ) : ( - - )} -
{t("or").toUpperCase()}
- setOpenCardModal(true)} /> */} - setOpenStripeModal(true)} /> +
+ + +

{t("will_be_charged", { strongText: ( - {t("trial_price", { price: price })} + + {t("trial_price", { + price: getPrice(activeSubPlan || null), + })} + ), })}

- {/* setOpenCardModal(false)} - onSuccess={onSuccess} - onError={onError} - /> */} - setOpenStripeModal(false)} - onSuccess={onSuccess} - onError={onError} - /> setOpenErrorModal(false)} diff --git a/src/components/PaymentPage/methods/PayPal/Button.tsx b/src/components/PaymentPage/methods/PayPal/Button.tsx new file mode 100644 index 0000000..78e32a5 --- /dev/null +++ b/src/components/PaymentPage/methods/PayPal/Button.tsx @@ -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 ( + + {t("payPal")} + + ); +} diff --git a/src/components/PaymentPage/methods/Stripe/Button.tsx b/src/components/PaymentPage/methods/Stripe/Button.tsx index 2f35277..cba41d3 100644 --- a/src/components/PaymentPage/methods/Stripe/Button.tsx +++ b/src/components/PaymentPage/methods/Stripe/Button.tsx @@ -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 ( - {/* Credit / Debit Card */} {t('stripe')} ) diff --git a/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx b/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx index 2487d31..244fc4e 100644 --- a/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx +++ b/src/components/PaymentPage/methods/Stripe/CheckoutForm.tsx @@ -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(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 ( -
+ diff --git a/src/components/PaymentPage/methods/Stripe/Modal.tsx b/src/components/PaymentPage/methods/Stripe/Modal.tsx index daa0eea..f38259e 100644 --- a/src/components/PaymentPage/methods/Stripe/Modal.tsx +++ b/src/components/PaymentPage/methods/Stripe/Modal.tsx @@ -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 | null>(null); const [clientSecret, setClientSecret] = useState(""); - 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); diff --git a/src/components/PaymentPage/results/SuccessPage/index.tsx b/src/components/PaymentPage/results/SuccessPage/index.tsx index ba9aed4..6d5e665 100644 --- a/src/components/PaymentPage/results/SuccessPage/index.tsx +++ b/src/components/PaymentPage/results/SuccessPage/index.tsx @@ -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 (
@@ -17,7 +23,9 @@ function PaymentSuccessPage(): JSX.Element { {t("auweb.pay_good.title")}

{t("auweb.pay_good.text1")}

- {t("auweb.pay_good.button")} + + {t("auweb.pay_good.button")} + ); } diff --git a/src/components/PaymentPage/results/index.tsx b/src/components/PaymentPage/results/index.tsx index e9356e8..4871444 100644 --- a/src/components/PaymentPage/results/index.tsx +++ b/src/components/PaymentPage/results/index.tsx @@ -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 <>; } diff --git a/src/components/PaymentPage/styles.css b/src/components/PaymentPage/styles.css index e734aca..89be04d 100644 --- a/src/components/PaymentPage/styles.css +++ b/src/components/PaymentPage/styles.css @@ -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; } diff --git a/src/components/Policy/index.tsx b/src/components/Policy/index.tsx index b62527d..f4053ea 100644 --- a/src/components/Policy/index.tsx +++ b/src/components/Policy/index.tsx @@ -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 (

{children}

- ) + ); } -export default Policy +export default Policy; diff --git a/src/components/PriceItem/index.tsx b/src/components/PriceItem/index.tsx index a930345..0b48af5 100644 --- a/src/components/PriceItem/index.tsx +++ b/src/components/PriceItem/index.tsx @@ -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 : ""}`; }; diff --git a/src/components/PriceList/index.tsx b/src/components/PriceList/index.tsx index fd51fb6..b3a1bd9 100644 --- a/src/components/PriceList/index.tsx +++ b/src/components/PriceList/index.tsx @@ -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(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(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 (
- {prices.map((price, idx) => ( - + {subPlans.map((plan, idx) => ( + ))}
- ) + ); } -export default PriceList +export default PriceList; diff --git a/src/components/PriceListPage/index.tsx b/src/components/PriceListPage/index.tsx index 554bc6d..69e6668 100644 --- a/src/components/PriceListPage/index.tsx +++ b/src/components/PriceListPage/index.tsx @@ -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([]); + 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 ( <> - -
- {t('choose_your_own_fee')} -

{t('aura.web.price_selection')}

-
- + +
+ {!!subPlans.length && ( + <> + + {t("choose_your_own_fee")} + +

{t("aura.web.price_selection")}

+
+
-
- +
+
-
+ + )} + {!subPlans.length && } +
- ) + ); } -export default PriceListPage +export default PriceListPage; diff --git a/src/components/SpecialWelcomeOffer/index.tsx b/src/components/SpecialWelcomeOffer/index.tsx index eea012e..2305a2b 100644 --- a/src/components/SpecialWelcomeOffer/index.tsx +++ b/src/components/SpecialWelcomeOffer/index.tsx @@ -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 { { - console.log("click"); - }} + onClick={handleMoreAbout} > Leo {t("au.more_llc.button")} diff --git a/src/components/StripePage/index.tsx b/src/components/StripePage/index.tsx new file mode 100644 index 0000000..dfa1ec4 --- /dev/null +++ b/src/components/StripePage/index.tsx @@ -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 | null>(null); + const [clientSecret, setClientSecret] = useState(""); + 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 ( +
+ {isLoading ? ( +
+ +
+ ) : null} + {stripePromise && clientSecret && ( + + + + )} +
+ ); +} diff --git a/src/components/StripePage/styles.module.css b/src/components/StripePage/styles.module.css new file mode 100644 index 0000000..dda5732 --- /dev/null +++ b/src/components/StripePage/styles.module.css @@ -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; +} diff --git a/src/components/SubscriptionPage/index.tsx b/src/components/SubscriptionPage/index.tsx index 1addcd2..11b7161 100644 --- a/src/components/SubscriptionPage/index.tsx +++ b/src/components/SubscriptionPage/index.tsx @@ -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")} ); - console.log({ itemPrice }); + return ( <>
-
+
{t("get_access")}
- {t("subscription_text", { policyLink })} + + <> + {t("auweb.agree.text1")} + {t("subscription_text", { policyLink })} + + ); diff --git a/src/components/SubscriptionPage/styles.module.css b/src/components/SubscriptionPage/styles.module.css index 97d1d54..15061b0 100644 --- a/src/components/SubscriptionPage/styles.module.css +++ b/src/components/SubscriptionPage/styles.module.css @@ -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; } diff --git a/src/components/WallpaperPage/index.tsx b/src/components/WallpaperPage/index.tsx index 3719670..7dd060a 100644 --- a/src/components/WallpaperPage/index.tsx +++ b/src/components/WallpaperPage/index.tsx @@ -47,7 +47,9 @@ function getZodiacParagraphs( {paragraph.title} {getTypeOfContent(paragraph.content) === "string" - ? paragraph.content.map((content, _index) =>

{content as string}

) + ? paragraph.content.map((content, _index) => ( +

{content as string}

+ )) : getZodiacParagraphs(paragraph.content as ZodiacParagraph[], depth)}
); @@ -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 { ))} - {/*

- {t("analysis_background")} -

*/} - {/* {forecasts.map((forecast) => ( -
-

- {forecast.category} -

-

{forecast.body}

-
- ))} */}
{getZodiacParagraphs(zodiacInfo?.paragraphs || [])}
diff --git a/src/locales/dev.ts b/src/locales/dev.ts index d9a557b..b428b63 100644 --- a/src/locales/dev.ts +++ b/src/locales/dev.ts @@ -35,7 +35,7 @@ export default { charged_only: "You will be charged only 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 ", + subscription_text: " Learn more about cancellation and refund policy in ", 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 ", 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...", diff --git a/src/routes.ts b/src/routes.ts index fd689fa..7ca5f05 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -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}"`); } diff --git a/src/services/price/index.ts b/src/services/price/index.ts new file mode 100644 index 0000000..52afca5 --- /dev/null +++ b/src/services/price/index.ts @@ -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]; +}; diff --git a/src/store/index.ts b/src/store/index.ts index b7df392..6288770 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -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, diff --git a/src/store/payment.ts b/src/store/payment.ts index c4d7de4..908ad06 100644 --- a/src/store/payment.ts +++ b/src/store/payment.ts @@ -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 diff --git a/src/types.ts b/src/types.ts index 9ff926b..e124ca4 100644 --- a/src/types.ts +++ b/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 { - 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";