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

View File

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

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 { AuthPayload } from "../types"
// import { getAuthHeaders } from "../utils"
export interface Payload {
locale: string

View File

@ -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

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

View File

@ -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,
},
};
}

View File

@ -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";

View File

@ -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);

View File

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

View File

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

View File

@ -22,6 +22,7 @@
margin: 64px auto 0;
display: flex;
flex-direction: column;
align-items: center;
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());
};
// const handleCompatibility = () => navigate(routes.client.compatibility());
const loadData = useCallback(async () => {
const right_bday =
typeof rightUser.birthDate === "string"

View File

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

View File

@ -48,8 +48,10 @@ const DatePickerItem: React.FC<DatePickerItemProps> = ({
};
useEffect(() => {
setTranslateY((data.indexOf(selectedValue) + (unit === "month" ? 12 : 0)) * -ITEM_HEIGHT)
}, [selectedValue, data, unit])
setTranslateY(
(data.indexOf(selectedValue) + (unit === "month" ? 12 : 0)) * -ITEM_HEIGHT
);
}, [selectedValue, data, unit]);
useEffect(() => {
if (unit === "month") {
@ -98,48 +100,6 @@ const DatePickerItem: React.FC<DatePickerItemProps> = ({
}
};
// const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
// if (!isMobile) {
// setTouchY(event.clientY);
// document.addEventListener("mousemove", handleMouseMove);
// document.addEventListener("mouseup", handleMouseUp);
// }
// };
// const handleMouseMove = (event: MouseEvent) => {
// const deltaY = event.clientY - touchY;
// handleScroll(deltaY);
// setTouchY(event.clientY);
// };
// const handleMouseUp = () => {
// resetMouseState();
// };
// const resetTouchState = () => {
// if (isMobile && scrollRef.current) {
// const selectedIndex = Math.round(-translateY / ITEM_HEIGHT);
// onSelect(data[selectedIndex]);
// // Limit the translateY to ensure it aligns with a valid item
// setTranslateY(-selectedIndex * ITEM_HEIGHT);
// setTouchY(0);
// }
// };
// const resetMouseState = () => {
// document.removeEventListener("mousemove", handleMouseMove);
// document.removeEventListener("mouseup", handleMouseUp);
// resetTouchState();
// };
// useEffect(() => {
// // Clean up mouse event listeners when the component unmounts
// return () => {
// resetMouseState();
// };
// }, []);
return (
<div className={styles["date-picker-viewport"]}>
<div className={styles["date-picker-wheel"]}>

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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 <></>;
}

View File

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

View File

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

View File

@ -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 : ""}`;
};

View File

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

View File

@ -1,46 +1,87 @@
import { useNavigate } from 'react-router-dom'
import routes from '@/routes'
import styles from './styles.module.css'
import UserHeader from '../UserHeader'
import { useDispatch, useSelector } from 'react-redux'
import { actions, selectors } from '@/store'
import Title from '../Title'
import { useTranslation } from 'react-i18next'
import EmailsList from '../EmailsList'
import PriceList from '../PriceList'
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
import styles from "./styles.module.css";
import UserHeader from "../UserHeader";
import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store";
import Title from "../Title";
import { useTranslation } from "react-i18next";
import EmailsList from "../EmailsList";
import PriceList from "../PriceList";
import { useApi } from "@/api";
import { useEffect, useState } from "react";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import Loader from "../Loader";
function PriceListPage(): JSX.Element {
const { t } = useTranslation()
const navigate = useNavigate()
const { t, i18n } = useTranslation();
const locale = i18n.language;
const api = useApi();
const navigate = useNavigate();
const dispatch = useDispatch();
const homeConfig = useSelector(selectors.selectHome);
const selectedPrice = useSelector(selectors.selectSelectedPrice)
const email = useSelector(selectors.selectEmail)
const selectedPrice = useSelector(selectors.selectSelectedPrice);
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
const email = useSelector(selectors.selectEmail);
useEffect(() => {
(async () => {
const { sub_plans } = await api.getSubscriptionPlans({ locale });
const plans = sub_plans
.filter(
(plan: ISubscriptionPlan) => plan.provider === "stripe" && plan.trial
)
.sort((a, b) => {
if (!a.trial || !b.trial) {
return 0;
}
if (a.trial.price_cents < b.trial.price_cents) {
return -1;
}
if (a.trial.price_cents > b.trial.price_cents) {
return 1;
}
return 0;
});
setSubPlans(plans);
})();
}, [api]);
const handleNext = () => {
dispatch(
actions.siteConfig.update({
home: { pathFromHome: homeConfig.pathFromHome, isShowNavbar: false },
})
);
navigate(routes.client.subscription())
}
navigate(routes.client.subscription());
};
return (
<>
<UserHeader email={email} />
<section className={`${styles.page} page`}>
<Title className={styles.title} variant='h2'>{t('choose_your_own_fee')}</Title>
<p className={styles.slogan}>{t('aura.web.price_selection')}</p>
<div className={styles['emails-list-container']}>
<EmailsList />
<UserHeader email={email} />
<section className={`${styles.page} page`}>
{!!subPlans.length && (
<>
<Title className={styles.title} variant="h2">
{t("choose_your_own_fee")}
</Title>
<p className={styles.slogan}>{t("aura.web.price_selection")}</p>
<div className={styles["emails-list-container"]}>
<EmailsList />
</div>
<div className={styles['price-list-container']}>
<PriceList activeItem={selectedPrice} click={handleNext} />
<div className={styles["price-list-container"]}>
<PriceList
activeItem={selectedPrice}
subPlans={subPlans}
click={handleNext}
/>
</div>
</section>
</>
)}
{!subPlans.length && <Loader />}
</section>
</>
)
);
}
export default PriceListPage
export default PriceListPage;

View File

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

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

View File

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

View File

@ -47,7 +47,9 @@ function getZodiacParagraphs(
{paragraph.title}
</Headline>
{getTypeOfContent(paragraph.content) === "string"
? paragraph.content.map((content, _index) => <p key={_index}>{content as string}</p>)
? paragraph.content.map((content, _index) => (
<p key={_index}>{content as string}</p>
))
: getZodiacParagraphs(paragraph.content as ZodiacParagraph[], depth)}
</div>
);
@ -58,7 +60,7 @@ function WallpaperPage(): JSX.Element {
const api = useApi();
const { i18n } = useTranslation();
const locale = i18n.language;
const token = useSelector(selectors.selectToken)
const token = useSelector(selectors.selectToken);
const {
user,
@ -111,20 +113,6 @@ function WallpaperPage(): JSX.Element {
</div>
))}
</div>
{/* <h1 className={styles["wallpaper-title"]}>
{t("analysis_background")}
</h1> */}
{/* {forecasts.map((forecast) => (
<div
key={forecast.category_name}
className={styles["wallpaper-forecast"]}
>
<h2 className={styles["wallpaper-subtitle"]}>
{forecast.category}
</h2>
<p className={styles["wallpaper-text"]}>{forecast.body}</p>
</div>
))} */}
<div className={styles["wallpaper-forecast"]}>
{getZodiacParagraphs(zodiacInfo?.paragraphs || [])}
</div>

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.",
purposes: "For entertaiment purposes only.",
get_access: "Get access",
subscription_text: "By proceeding, you agree that if you do not cancel your subscription before the end of the 7-day trial period, you will be automatically charged nineteen US dollars zero cents every 2 weeks until you cancel the subscription in the settings. Learn more about cancellation and refund policy in <policyLink>",
subscription_text: " Learn more about cancellation and refund policy in <policyLink>",
subscription_policy: "Subscription policy",
company_name: "Wit Apps LLC, California, US",
choose_payment: "Choose Payment Method",
@ -81,6 +81,7 @@ export default {
you_and: "You and <user>",
sign: "Sign",
stripe: "Stripe",
payPal: "PayPal",
'aura-10_breath-button': "Increase up to 10%. Practice for the Energy of Money",
'aura-money_compatibility-button': "low MONEY energy. Determine who drains your energy",
"breathe-subtitle": "Breathing practice will help improve your aura. Breath in the positive energy, breathe out the negative...",

View File

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

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";
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,

View File

@ -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

View File

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