This commit is contained in:
Daniil Chemerkin 2024-06-13 00:59:26 +00:00
parent 97ea46efd8
commit bafd8cda00
44 changed files with 983 additions and 395 deletions

BIN
public/amazon-pay-mark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/google-pay-mark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/link-pay-mark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -171,6 +171,8 @@ export interface ICreateAuthorizePayload {
source: ESourceAuthorization;
profile?: Partial<ICreateAuthorizeUser>;
partner?: Partial<Exclude<ICreateAuthorizeUser, "relationship_status">>;
sign?: boolean;
signDate?: string;
}
export interface ICreateAuthorizeResponse {

View File

@ -118,12 +118,6 @@ function EmailEnterPage({
Enter your email
</Title>
<p className={styles["not-share"]}>{t("we_dont_share")}</p>
<NameInput
value={name}
placeholder="Your name"
onValid={handleValidName}
onInvalid={() => setIsValidName(!isRequiredName)}
/>
<EmailInput
name="email"
value={email}
@ -131,6 +125,12 @@ function EmailEnterPage({
onValid={handleValidEmail}
onInvalid={() => setIsValidEmail(false)}
/>
<NameInput
value={name}
placeholder="Your name"
onValid={handleValidName}
onInvalid={() => setIsValidName(!isRequiredName)}
/>
<Policy sizing="medium" className={styles.policy}>
{t("_continue_agree", {
eulaLink: (

View File

@ -343,7 +343,7 @@ function HomePage(): JSX.Element {
<Slider>
{predictionMoonsPeriods.map((item, index) => (
<NameHoroscopeSlider
data={{ ...item, name: user.username || "I Am" }}
data={{ ...item, name: user.username || "" }}
key={index}
onClick={() => {
handleNameHoroscope(item);

View File

@ -1,121 +0,0 @@
import { useEffect, useState } from "react";
import {
PaymentRequestButtonElement,
useStripe,
useElements,
} from "@stripe/react-stripe-js";
import { PaymentRequest } from "@stripe/stripe-js";
import styles from "./styles.module.css";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
import { IPaywallProduct } from "@/api/resources/Paywall";
interface ApplePayButtonProps {
activeProduct: IPaywallProduct | null;
client_secret: string;
subscriptionReceiptId?: string;
returnUrl?: string;
setCanMakePayment?: (isCanMakePayment: boolean) => void;
}
function ApplePayButton({
activeProduct,
client_secret,
subscriptionReceiptId,
returnUrl,
setCanMakePayment,
}: ApplePayButtonProps) {
const stripe = useStripe();
const elements = useElements();
const dispatch = useDispatch();
const navigate = useNavigate();
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest | null>(
null
);
const getAmountFromProduct = (subPlan: IPaywallProduct) => {
if (subPlan.isTrial) {
return subPlan.trialPrice;
}
return subPlan.price;
};
useEffect(() => {
if (!stripe || !elements || !activeProduct) {
return;
}
const pr = stripe.paymentRequest({
country: "US",
currency: "usd",
total: {
label: activeProduct.name || "Subscription",
amount: getAmountFromProduct(activeProduct),
},
requestPayerName: true,
requestPayerEmail: true,
});
pr.canMakePayment().then((result) => {
if (result) {
setPaymentRequest(pr);
setCanMakePayment?.(true);
}
});
pr.on("paymentmethod", async (e) => {
const { error: stripeError, paymentIntent } =
await stripe.confirmCardPayment(
client_secret,
{
payment_method: e.paymentMethod.id,
},
{ handleActions: false }
);
paymentIntent;
if (stripeError) {
// Show error to your customer (e.g., insufficient funds)
navigate(
`${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=failed`
);
return e.complete("fail");
}
navigate(
returnUrl ||
`${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=succeeded`
);
e.complete("success");
// Show a success message to your customer
// There's a risk of the customer closing the window before callback
// execution. Set up a webhook or plugin to listen for the
// payment_intent.succeeded event that handles any business critical
// post-payment actions.
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
client_secret,
dispatch,
elements,
navigate,
stripe,
subscriptionReceiptId,
]);
return (
<>
{paymentRequest && (
<PaymentRequestButtonElement
className={styles["stripe-element"]}
options={{
paymentRequest,
style: { paymentRequestButton: { height: "60px" } },
}}
/>
)}
</>
);
}
export default ApplePayButton;

View File

@ -0,0 +1,112 @@
import { useMemo, useState } from "react";
import styles from "./styles.module.css";
import {
useStripe,
useElements,
ExpressCheckoutElement,
} from "@stripe/react-stripe-js";
import {
AvailablePaymentMethods,
StripeExpressCheckoutElementReadyEvent,
} from "@stripe/stripe-js";
import { checkExpressCheckoutStripeFormAvailable } from "@/data/paymentMethods";
interface IExpressCheckoutStripeProps {
clientSecret: string;
returnUrl?: string;
isHide?: boolean;
onAvailable?: (
isAvailable: boolean,
availableMethods: AvailablePaymentMethods | undefined
) => void;
onChangeLoading?: (isLoading: boolean) => void;
}
function ExpressCheckoutStripe({
clientSecret,
returnUrl = "https://${window.location.host}/payment/result/",
isHide = false,
onAvailable,
onChangeLoading,
}: IExpressCheckoutStripeProps) {
const stripe = useStripe();
const elements = useElements();
const [errorMessage, setErrorMessage] = useState<string>();
const [isAvailable, setIsAvailable] = useState(false);
const isHideForm = useMemo(
() => isHide || !isAvailable,
[isAvailable, isHide]
);
const onConfirm = async () =>
// event: StripeExpressCheckoutElementConfirmEvent
{
if (!stripe || !elements) {
// Stripe.js hasn't loaded yet.
// Make sure to disable form submission until Stripe.js has loaded.
return;
}
const { error: submitError } = await elements.submit();
if (submitError) {
setErrorMessage(submitError.message);
return;
}
// // Create the PaymentIntent and obtain clientSecret
// const res = await fetch("/create-intent", {
// method: "POST",
// });
// const { client_secret: clientSecret } = await res.json();
// Confirm the PaymentIntent using the details collected by the Express Checkout Element
const { error } = await stripe.confirmPayment({
// `elements` instance used to create the Express Checkout Element
elements,
// `clientSecret` from the created PaymentIntent
clientSecret,
confirmParams: {
return_url: returnUrl,
},
});
if (error) {
// This point is only reached if there's an immediate error when
// confirming the payment. Show the error to your customer (for example, payment details incomplete)
setErrorMessage(error.message);
} else {
// The payment UI automatically closes with a success animation.
// Your customer is redirected to your `return_url`.
}
};
const onReady = (event: StripeExpressCheckoutElementReadyEvent) => {
const _isAvailable = checkExpressCheckoutStripeFormAvailable(
event.availablePaymentMethods
);
setIsAvailable(_isAvailable);
onAvailable && onAvailable(_isAvailable, event.availablePaymentMethods);
onChangeLoading && onChangeLoading(false);
};
return (
<div className={`${styles.container} ${isHideForm ? styles.hide : ""}`}>
<ExpressCheckoutElement
onReady={onReady}
onLoadError={() => onChangeLoading && onChangeLoading(false)}
onConfirm={onConfirm}
options={{
layout: {
maxColumns: 1,
overflow: "never",
},
paymentMethodOrder: ["apple_pay", "google_pay", "amazon_pay", "link"],
}}
/>
{errorMessage && <p className={styles.error}>{errorMessage}</p>}
</div>
);
}
export default ExpressCheckoutStripe;

View File

@ -0,0 +1,16 @@
.container {
width: 100%;
}
.hide {
height: 0;
visibility: hidden;
}
.error {
width: 100%;
color: #FF5758;
text-align: center;
font-size: 14px;
margin-top: 8px;
}

View File

@ -0,0 +1,33 @@
import { IPaywallProduct } from "@/api/resources/Paywall";
import { useCanUseStripeButton } from "@/hooks/payment/useCanUseStripeButton";
import { actions } from "@/store";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
interface ICheckAvailableStripeButtonProps {
activeProduct: IPaywallProduct | null;
clientSecret: string;
}
function CheckAvailableStripeButton({
activeProduct,
clientSecret,
}: ICheckAvailableStripeButtonProps) {
const dispatch = useDispatch();
const { paymentRequest, availableMethods } = useCanUseStripeButton({
activeProduct,
client_secret: clientSecret,
});
useEffect(() => {
if (paymentRequest && availableMethods) {
dispatch(
actions.payment.updateStripeButton({ paymentRequest, availableMethods })
);
}
}, [availableMethods, dispatch, paymentRequest]);
return <></>;
}
export default CheckAvailableStripeButton;

View File

@ -0,0 +1,27 @@
import { PaymentRequestButtonElement } from "@stripe/react-stripe-js";
import { CanMakePaymentResult, PaymentRequest } from "@stripe/stripe-js";
import styles from "./styles.module.css";
export type TCanMakePaymentResult = CanMakePaymentResult | null;
interface ApplePayButtonProps {
paymentRequest: PaymentRequest;
}
function StripeButton({ paymentRequest }: ApplePayButtonProps) {
return (
<>
{paymentRequest && (
<PaymentRequestButtonElement
className={styles["stripe-element"]}
options={{
paymentRequest,
style: { paymentRequestButton: { height: "60px" } },
}}
/>
)}
</>
);
}
export default StripeButton;

View File

@ -1,17 +0,0 @@
import styles from "./styles.module.css";
import Title from "@/components/Title";
interface ITotalTodayProps {
total: string;
}
function TotalToday({ total }: ITotalTodayProps): JSX.Element {
return (
<div className={styles.container}>
<Title className={styles.text} variant="h3">{"Total today:"}</Title>
<Title className={styles.text} variant="h3">{total}</Title>
</div>
);
}
export default TotalToday;

View File

@ -1,17 +0,0 @@
.container {
width: 100%;
padding: 16px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
background-color: #e7f5ee;
border-radius: 7px;
}
.text {
font-size: 16px;
font-weight: 700;
color: #000;
margin: 0;
}

View File

@ -1,37 +0,0 @@
import { useTranslation } from "react-i18next";
import styles from "./styles.module.css";
import TotalToday from "./TotalToday";
import ApplePayButton from "../PaymentPage/methods/ApplePayButton";
import { IPaywallProduct } from "@/api/resources/Paywall";
interface ISubPlanInformationProps {
product: IPaywallProduct;
client_secret?: string;
}
const getPrice = (product: IPaywallProduct): string => {
return `$${
(product.trialPrice === 100 ? 99 : product.trialPrice || 0) / 100
}`;
};
function SubPlanInformation({
product,
client_secret,
}: ISubPlanInformationProps): JSX.Element {
const { t } = useTranslation();
return (
<div className={styles.container}>
<TotalToday total={getPrice(product)} />
{client_secret && (
<ApplePayButton activeProduct={product} client_secret={client_secret} />
)}
<p className={styles.description}>
{t("auweb.pay.information").replaceAll("%@", getPrice(product))}.
</p>
</div>
);
}
export default SubPlanInformation;

View File

@ -1,31 +0,0 @@
.container {
width: 100%;
max-width: 300px;
display: flex;
flex-direction: column;
gap: 20px;
}
.description {
font-size: 13px;
color: #666666;
text-align: left;
font-weight: 400;
line-height: 16px;
padding-bottom: 16px;
}
.pay-pal-button {
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background-color: #ffc43a;
border-radius: 7px;
}
.errors {
color: red;
text-align: center;
}

View File

@ -0,0 +1,34 @@
import styles from "./styles.module.css";
interface ICheckboxProps {
checked: boolean;
onChange: () => void;
}
function Checkbox({ checked, onChange }: ICheckboxProps) {
return (
<div className={styles["checkbox-wrapper-4"]}>
<input
checked={checked}
className={styles["inp-cbx"]}
id="morning"
type="checkbox"
onChange={onChange}
/>
<label className={styles["cbx"]} htmlFor="morning">
<span>
<svg width="12px" height="10px">
<use xlinkHref="#check-4"></use>
</svg>
</span>
</label>
<svg className={styles["inline-svg"]}>
<symbol id="check-4" viewBox="0 0 12 10">
<polyline points="1.5 6 4.5 9 10.5 1"></polyline>
</symbol>
</svg>
</div>
);
}
export default Checkbox;

View File

@ -0,0 +1,92 @@
.checkbox-wrapper-4 * {
box-sizing: border-box;
}
.checkbox-wrapper-4 .cbx {
-webkit-user-select: none;
user-select: none;
cursor: pointer;
padding: 2px;
border-radius: 6px;
overflow: hidden;
transition: all 0.2s ease;
display: inline-block;
}
.checkbox-wrapper-4 .cbx span {
float: left;
vertical-align: middle;
transform: translate3d(0, 0, 0);
}
.checkbox-wrapper-4 .cbx span:first-child {
position: relative;
width: 20px;
height: 20px;
border-radius: 4px;
transform: scale(1);
border: 1px solid #484848;
transition: all 0.2s ease;
box-shadow: 0 1px 1px rgba(0,16,75,0.05);
}
.checkbox-wrapper-4 .cbx span:first-child svg {
position: absolute;
top: 50%;
left: 50%;
fill: none;
stroke: #fff;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 16px;
stroke-dashoffset: 16px;
transition: all 0.3s ease;
transition-delay: 0.1s;
transform: translate3d(-50%, -50%, 0);
}
.checkbox-wrapper-4 .cbx span:last-child {
padding-left: 8px;
line-height: 18px;
}
.checkbox-wrapper-4 .inp-cbx {
position: absolute;
visibility: hidden;
}
.checkbox-wrapper-4 .inp-cbx:checked + .cbx span:first-child {
background: #07f;
border-color: #07f;
animation: wave-4 0.4s ease;
}
.checkbox-wrapper-4 .inp-cbx:checked + .cbx span:first-child svg {
stroke-dashoffset: 0;
}
.checkbox-wrapper-4 .inline-svg {
position: absolute;
width: 0;
height: 0;
pointer-events: none;
user-select: none;
}
@media screen and (max-width: 640px) {
.checkbox-wrapper-4 .cbx {
width: 100%;
display: inline-block;
}
}
@-moz-keyframes wave-4 {
50% {
transform: scale(0.9);
}
}
@-webkit-keyframes wave-4 {
50% {
transform: scale(0.9);
}
}
@-o-keyframes wave-4 {
50% {
transform: scale(0.9);
}
}
@keyframes wave-4 {
50% {
transform: scale(0.9);
}
}

View File

@ -0,0 +1,45 @@
import Checkbox from "../Checkbox";
import styles from "./styles.module.css";
import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store";
interface IPrivacyPolicyProps {
containerClassName?: string;
}
function PrivacyPolicy({ containerClassName = "" }: IPrivacyPolicyProps) {
const dispatch = useDispatch();
const { checked } = useSelector(selectors.selectPrivacyPolicy);
const handleChange = () => {
dispatch(actions.privacyPolicy.updateChecked(!checked));
};
return (
<div className={`${styles.container} ${containerClassName}`}>
<Checkbox checked={checked} onChange={handleChange} />
<p className={styles.text}>
I agree to the{" "}
<a
href="https://aura.wit.life/privacy"
target="_blank"
rel="noopener noreferrer"
>
Privacy Policy
</a>
,{" "}
<a
href="https://aura.wit.life/terms"
target="_blank"
rel="noopener noreferrer"
>
Terms of use
</a>{" "}
and to the use of cookies and tracking technologies, that require your
consent
</p>
</div>
);
}
export default PrivacyPolicy;

View File

@ -0,0 +1,16 @@
.container {
width: 100%;
display: flex;
flex-direction: row;
gap: 8px;
}
.text {
font-size: 14px;
line-height: 125%;
color: #515151;
}
.text > a {
text-decoration: underline;
}

View File

@ -0,0 +1,24 @@
function ErrorIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="12" cy="12" r="11" strokeWidth={1.5} stroke="#e12b2b" />
<rect x="11.4297" y="6.85645" width="1.5" height="8" fill="#e12b2b" />
<rect
x="11.4297"
y="16.5703"
width="1.4"
height="1.4"
rx="0.571429"
fill="#e12b2b"
/>
</svg>
);
}
export default ErrorIcon;

View File

@ -0,0 +1,28 @@
import React from "react";
import ErrorIcon from "./ErrorIcon";
import styles from "./styles.module.css";
interface IToastProps {
variant: "error";
children: React.ReactNode;
classNameContainer?: string;
classNameToast?: string;
}
function Toast({
variant,
children,
classNameContainer = "",
classNameToast = "",
}: IToastProps) {
return (
<div className={`${styles.container} ${classNameContainer}`}>
<div className={`${styles.toast} ${styles[variant]} ${classNameToast}`}>
{variant === "error" && <ErrorIcon />}
{children}
</div>
</div>
);
}
export default Toast;

View File

@ -0,0 +1,26 @@
.toast {
width: 100%;
display: grid;
grid-template-columns: 24px 1fr;
gap: 6px;
align-items: center;
padding: 16px;
border-radius: 12px;
font-size: 14px;
color: #000;
animation: appearance .8s linear(0 0%, 0 1.8%, 0.01 3.6%, 0.08 10.03%, 0.15 14.25%, 0.2 14.34%, 0.31 14.14%, 0.41 17.21%, 0.49 19.04%, 0.58 20.56%, 0.66 22.07%, 0.76 23.87%, 0.84 26.07%, 0.93 28.04%, 1.03 31.14%, 1.09 37.31%, 1.09 44.28%, 1.02 49.41%, 0.96 55%, 0.98 64%, 0.99 74.4%, 1 86.4%, 1 100%);
animation-fill-mode: forwards;
}
.toast.error {
background-color: #ffdcdc;
}
@keyframes appearance {
0% {
transform: translateY(100%);
}
100% {
transform: translateY(0);
}
}

View File

@ -133,19 +133,19 @@ function EmailEnterPage({
Enter your email
</Title>
<p className={styles["not-share"]}>{t("we_dont_share")}</p>
<EmailInput
name="email"
value={email}
placeholder={t("your_email")}
onValid={handleValidEmail}
onInvalid={() => setIsValidEmail(false)}
/>
<NameInput
value={name}
placeholder="Your name"
onValid={handleValidName}
onInvalid={() => setIsValidName(!isRequiredName)}
/>
<EmailInput
name="email"
value={email}
placeholder={t("your_email")}
onValid={handleValidEmail}
onInvalid={() => setIsValidEmail(false)}
/>
<QuestionnaireGreenButton
className={styles.button}
onClick={handleClick}

View File

@ -3,14 +3,16 @@ import Title from "@/components/Title";
import { Gender } from "@/data";
import { EProductKeys } from "@/data/products";
import routes from "@/routes";
import { actions } from "@/store";
import { actions, selectors } from "@/store";
import { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import BackgroundTopBlob from "../../ui/BackgroundTopBlob";
import { useDynamicSize } from "@/hooks/useDynamicSize";
import Header from "../../components/Header";
import { genders } from "../../data/genders";
import PrivacyPolicy from "../../components/PrivacyPolicy";
import Toast from "../../components/Toast";
interface IGenderPageProps {
productKey?: EProductKeys;
@ -22,16 +24,26 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element {
const { targetId } = useParams();
const { width: pageWidth, elementRef: pageRef } = useDynamicSize({});
const [selectedGender, setSelectedGender] = useState<Gender | null>(null);
const { checked: privacyPolicyChecked } = useSelector(
selectors.selectPrivacyPolicy
);
useEffect(() => {
const isShowTryApp = targetId === "i";
dispatch(actions.userConfig.addIsShowTryApp(isShowTryApp));
}, [dispatch, targetId]);
const selectGender = async (gender: Gender) => {
setSelectedGender(gender);
useEffect(() => {
if (privacyPolicyChecked && selectedGender) {
handleNext();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [privacyPolicyChecked, selectedGender]);
const handleNext = async () => {
if (!selectedGender) return;
await new Promise((resolve) => setTimeout(resolve, 1000));
dispatch(actions.questionnaire.update({ gender: gender.id }));
dispatch(actions.questionnaire.update({ gender: selectedGender.id }));
if (productKey === EProductKeys["moons.pdf.aura"]) {
return navigate(routes.client.epeBirthdate());
}
@ -41,6 +53,18 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element {
navigate(`/v1/questionnaire/profile/flowChoice`);
};
const selectGender = async (gender: Gender) => {
if (selectedGender?.id === gender.id) {
setSelectedGender(null);
} else {
setSelectedGender(gender);
}
if (!privacyPolicyChecked) {
return;
}
handleNext();
};
const getButtonBGColor = (gender: Gender): string => {
const { colorAssociation } = gender;
if (Array.isArray(colorAssociation)) {
@ -114,6 +138,12 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element {
</div>
))}
</div>
<PrivacyPolicy containerClassName={styles["privacy-policy"]} />
{selectedGender && !privacyPolicyChecked && (
<Toast classNameContainer={styles["toast-container"]} variant="error">
To continue, please accept our terms and policies
</Toast>
)}
</section>
);
}

View File

@ -106,18 +106,43 @@
.gender--selected {
transform: scale(1.1);
/* animation: gender-click 1s linear; */
/* animation: gender-click 1.4s linear; */
}
.gender--selected .gender__slide-element {
left: calc(100% - 27px - 10px);
/* animation: gender-slide 1.4s linear; */
}
.privacy-policy {
max-width: 316px;
margin-top: 26px;
}
.toast-container {
margin-top: 16px;
}
@keyframes gender-click {
0% {
transform: scale(1);
}
100% {
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
@keyframes gender-slide {
0% {
left: 10px;
}
50% {
left: calc(100% - 27px - 10px);
}
100% {
left: 10px;
}
}

View File

@ -4,9 +4,8 @@ import PaymentMethodsChoice from "../PaymentMethodsChoice";
import { useEffect, useMemo, useState } from "react";
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
import { Elements } from "@stripe/react-stripe-js";
import ApplePayButton from "@/components/PaymentPage/methods/ApplePayButton";
import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
import { Stripe, loadStripe } from "@stripe/stripe-js";
import { AvailablePaymentMethods, Stripe, loadStripe } from "@stripe/stripe-js";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
import { useNavigate } from "react-router-dom";
@ -16,6 +15,7 @@ import SecurityPayments from "../SecurityPayments";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { useMakePayment } from "@/hooks/payment/useMakePayment";
import ExpressCheckoutStripe from "@/components/PaymentPage/methods/ExpressCheckoutStripe";
interface IPaymentModalProps {
activeProduct?: IPaywallProduct;
@ -41,14 +41,14 @@ function PaymentModal({
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
const _activeProduct = activeProduct ? activeProduct : activeProductFromStore;
const { products, paywallId, placementId } = usePaywall({ placementKey });
const {
paymentIntentId,
clientSecret,
returnUrl: checkoutUrl,
paymentType,
publicKey,
isLoading,
isLoading: isLoadingPayment,
error,
} = useMakePayment({
productId: _activeProduct?._id || "",
@ -57,16 +57,27 @@ function PaymentModal({
returnPaidUrl: returnUrl,
});
const [availableMethods, setAvailableMethods] = useState<
AvailablePaymentMethods | undefined
>();
const [isLoadingExpressCheckout, setIsLoadingExpressCheckout] =
useState(true);
const isLoading = useMemo(() => {
return isLoadingPayment || isLoadingExpressCheckout;
}, [isLoadingPayment, isLoadingExpressCheckout]);
if (checkoutUrl?.length) {
window.location.href = checkoutUrl;
}
const paymentMethodsButtons = useMemo(() => {
return paymentMethods;
}, []);
return paymentMethods(availableMethods || null);
}, [availableMethods]);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
EPaymentMethod.PAYMENT_BUTTONS
paymentMethodsButtons[0].id
);
const onSelectPaymentMethod = (method: EPaymentMethod) => {
@ -86,15 +97,16 @@ function PaymentModal({
})();
}, [_activeProduct, navigate, products, publicKey]);
if (isLoading) {
return (
<div className={styles["payment-modal"]}>
<div className={styles["payment-loader"]}>
<Loader />
</div>
</div>
);
}
const onAvailableExpressCheckout = (
isAvailable: boolean,
availableMethods: AvailablePaymentMethods | undefined
) => {
if (isAvailable && availableMethods) {
setAvailableMethods(availableMethods);
return setSelectedPaymentMethod(EPaymentMethod.PAYMENT_BUTTONS);
}
return setAvailableMethods(undefined);
};
if (error?.length) {
return (
@ -107,60 +119,75 @@ function PaymentModal({
}
return (
<div className={styles["payment-modal"]}>
<Title variant="h3" className={styles.title}>
Choose payment method
</Title>
<PaymentMethodsChoice
paymentMethods={paymentMethodsButtons}
selectedPaymentMethod={selectedPaymentMethod}
onSelectPaymentMethod={onSelectPaymentMethod}
/>
{_activeProduct && (
<div>
{!noTrial && (
<>
<p className={styles["sub-plan-description"]}>
You will be charged only{" "}
<b>${getPrice(_activeProduct)} for your 3-day trial.</b>
</p>
<p className={styles["sub-plan-description"]}>
We`ll <b>email you a reminder</b> before your trial period ends.
</p>
</>
)}
<p className={styles["sub-plan-description"]}>
Cancel anytime. The charge will appear on your bill as witapps.
</p>
<>
{isLoading && (
<div className={styles["payment-modal"]}>
<div className={styles["payment-loader"]}>
<Loader />
</div>
</div>
)}
<div className={styles["payment-method-container"]}>
{stripePromise && clientSecret && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
{selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
<div className={styles["payment-method"]}>
<ApplePayButton
activeProduct={_activeProduct}
client_secret={clientSecret}
subscriptionReceiptId={paymentIntentId}
/>
</div>
<div
className={`${styles["payment-modal"]} ${isLoading ? styles.hide : ""}`}
>
<Title variant="h3" className={styles.title}>
Choose payment method
</Title>
<PaymentMethodsChoice
paymentMethods={paymentMethodsButtons}
selectedPaymentMethod={selectedPaymentMethod}
onSelectPaymentMethod={onSelectPaymentMethod}
/>
{_activeProduct && (
<div>
{!noTrial && (
<>
<p className={styles["sub-plan-description"]}>
You will be charged only{" "}
<b>${getPrice(_activeProduct)} for your 3-day trial.</b>
</p>
<p className={styles["sub-plan-description"]}>
We`ll <b>email you a reminder</b> before your trial period
ends.
</p>
</>
)}
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
<CheckoutForm
confirmType={paymentType}
subscriptionReceiptId={paymentIntentId}
returnUrl={returnUrl}
/>
)}
</Elements>
<p className={styles["sub-plan-description"]}>
Cancel anytime. The charge will appear on your bill as witapps.
</p>
</div>
)}
<div className={styles["payment-method-container"]}>
{stripePromise && clientSecret && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<ExpressCheckoutStripe
clientSecret={clientSecret}
returnUrl={returnUrl}
isHide={
selectedPaymentMethod !== EPaymentMethod.PAYMENT_BUTTONS
}
onAvailable={(_isAvailable, _availableMethods) =>
onAvailableExpressCheckout(_isAvailable, _availableMethods)
}
onChangeLoading={(isLoading) =>
setIsLoadingExpressCheckout(isLoading)
}
/>
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
<CheckoutForm
confirmType={paymentType}
subscriptionReceiptId={paymentIntentId}
returnUrl={returnUrl}
/>
)}
</Elements>
)}
</div>
<SecurityPayments />
<p className={styles.address}>1123 Rimer Dr Moraga, California 94556</p>
</div>
<SecurityPayments />
<p className={styles.address}>500 N RAINBOW BLVD LAS VEGAS, NV 89107</p>
</div>
</>
);
}

View File

@ -8,6 +8,12 @@
color: #2f2e37;
}
.payment-modal.hide {
min-height: 0;
height: 0;
opacity: 0;
}
.title {
font-weight: 700;
font-size: 20px;
@ -31,6 +37,7 @@
.address {
margin-bottom: 24px;
text-transform: uppercase;
}
.payment-method {

View File

@ -2,7 +2,7 @@ import styles from "./styles.module.css"
function PaymentAddress() {
return (
<p className={styles.address}>500 N RAINBOW BLVD LAS VEGAS, NV 89107</p>
<p className={styles.address}>1123 Rimer Dr Moraga, California 94556</p>
)
}

View File

@ -4,4 +4,5 @@
text-align: center;
margin-top: 9px;
color: rgb(130, 130, 130);
text-transform: uppercase;
}

View File

@ -34,7 +34,7 @@ function PaymentForm({
)}
</div>
<SecurityPayments />
<p className={styles.address}>500 N RAINBOW BLVD LAS VEGAS, NV 89107</p>
<p className={styles.address}>1123 Rimer Dr Moraga, California 94556</p>
</div>
);
}

View File

@ -41,4 +41,5 @@
.address {
color: gray;
font-size: 10px;
text-transform: uppercase;
}

View File

@ -4,9 +4,8 @@ import PaymentMethodsChoice from "../PaymentMethodsChoice";
import { useEffect, useMemo, useState } from "react";
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
import { Elements } from "@stripe/react-stripe-js";
import ApplePayButton from "@/components/PaymentPage/methods/ApplePayButton";
import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
import { Stripe, loadStripe } from "@stripe/stripe-js";
import { AvailablePaymentMethods, Stripe, loadStripe } from "@stripe/stripe-js";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
import Loader from "@/components/Loader";
@ -14,6 +13,9 @@ import SecurityPayments from "../SecurityPayments";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { useMakePayment } from "@/hooks/payment/useMakePayment";
import ExpressCheckoutStripe from "@/components/PaymentPage/methods/ExpressCheckoutStripe";
import routes from "@/routes";
import { useNavigate } from "react-router-dom";
interface IPaymentModalProps {
activeProduct?: IPaywallProduct;
@ -35,6 +37,7 @@ function PaymentModal({
returnUrl,
placementKey = EPlacementKeys["aura.placement.main"],
}: IPaymentModalProps) {
const navigate = useNavigate();
const [stripePromise, setStripePromise] =
useState<Promise<Stripe | null> | null>(null);
@ -50,7 +53,7 @@ function PaymentModal({
returnUrl: checkoutUrl,
paymentType,
publicKey,
isLoading,
isLoading: isLoadingPayment,
error,
} = useMakePayment({
productId: _activeProduct?._id || "",
@ -59,19 +62,27 @@ function PaymentModal({
returnPaidUrl: returnUrl,
});
const [availableMethods, setAvailableMethods] = useState<
AvailablePaymentMethods | undefined
>();
const [isLoadingExpressCheckout, setIsLoadingExpressCheckout] =
useState(true);
const isLoading = useMemo(() => {
return isLoadingPayment || isLoadingExpressCheckout;
}, [isLoadingPayment, isLoadingExpressCheckout]);
if (checkoutUrl?.length) {
window.location.href = checkoutUrl;
}
const paymentMethodsButtons = useMemo(() => {
// return paymentMethods.filter(
// (method) => method.id !== EPaymentMethod.PAYMENT_BUTTONS
// );
return paymentMethods;
}, []);
return paymentMethods(availableMethods || null);
}, [availableMethods]);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
EPaymentMethod.PAYMENT_BUTTONS
paymentMethodsButtons[0].id
);
const onSelectPaymentMethod = (method: EPaymentMethod) => {
@ -82,18 +93,25 @@ function PaymentModal({
(async () => {
if (!products?.length || !publicKey) return;
setStripePromise(loadStripe(publicKey));
const isActiveProduct = products.find(
(product) => product._id === _activeProduct?._id
);
if (!_activeProduct || !isActiveProduct) {
navigate(routes.client.trialChoice());
}
})();
}, [products, publicKey]);
}, [_activeProduct, navigate, products, publicKey]);
if (isLoading) {
return (
<div className={styles["payment-modal"]}>
<div className={styles["payment-loader"]}>
<Loader />
</div>
</div>
);
}
const onAvailableExpressCheckout = (
isAvailable: boolean,
availableMethods: AvailablePaymentMethods | undefined
) => {
if (isAvailable && availableMethods) {
setAvailableMethods(availableMethods);
return setSelectedPaymentMethod(EPaymentMethod.PAYMENT_BUTTONS);
}
return setAvailableMethods(undefined);
};
if (error?.length) {
return (
@ -106,60 +124,75 @@ function PaymentModal({
}
return (
<div className={styles["payment-modal"]}>
<Title variant="h3" className={styles.title}>
Choose payment method
</Title>
<PaymentMethodsChoice
paymentMethods={paymentMethodsButtons}
selectedPaymentMethod={selectedPaymentMethod}
onSelectPaymentMethod={onSelectPaymentMethod}
/>
{_activeProduct && (
<div>
{!noTrial && (
<>
<p className={styles["sub-plan-description"]}>
You will be charged only{" "}
<b>${getPrice(_activeProduct)} for your 3-day trial.</b>
</p>
<p className={styles["sub-plan-description"]}>
We`ll <b>email you a reminder</b> before your trial period ends.
</p>
</>
)}
<p className={styles["sub-plan-description"]}>
Cancel anytime. The charge will appear on your bill as witapps.
</p>
<>
{isLoading && (
<div className={styles["payment-modal"]}>
<div className={styles["payment-loader"]}>
<Loader />
</div>
</div>
)}
<div className={styles["payment-method-container"]}>
{stripePromise && clientSecret && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
{selectedPaymentMethod === EPaymentMethod.PAYMENT_BUTTONS && (
<div className={styles["payment-method"]}>
<ApplePayButton
activeProduct={_activeProduct}
client_secret={clientSecret}
subscriptionReceiptId={paymentIntentId}
/>
</div>
<div
className={`${styles["payment-modal"]} ${isLoading ? styles.hide : ""}`}
>
<Title variant="h3" className={styles.title}>
Choose payment method
</Title>
<PaymentMethodsChoice
paymentMethods={paymentMethodsButtons}
selectedPaymentMethod={selectedPaymentMethod}
onSelectPaymentMethod={onSelectPaymentMethod}
/>
{_activeProduct && (
<div>
{!noTrial && (
<>
<p className={styles["sub-plan-description"]}>
You will be charged only{" "}
<b>${getPrice(_activeProduct)} for your 3-day trial.</b>
</p>
<p className={styles["sub-plan-description"]}>
We`ll <b>email you a reminder</b> before your trial period
ends.
</p>
</>
)}
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
<CheckoutForm
confirmType={paymentType}
subscriptionReceiptId={paymentIntentId}
returnUrl={returnUrl}
/>
)}
</Elements>
<p className={styles["sub-plan-description"]}>
Cancel anytime. The charge will appear on your bill as witapps.
</p>
</div>
)}
<div className={styles["payment-method-container"]}>
{stripePromise && clientSecret && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<ExpressCheckoutStripe
clientSecret={clientSecret}
returnUrl={returnUrl}
isHide={
selectedPaymentMethod !== EPaymentMethod.PAYMENT_BUTTONS
}
onAvailable={(_isAvailable, _availableMethods) =>
onAvailableExpressCheckout(_isAvailable, _availableMethods)
}
onChangeLoading={(isLoading) =>
setIsLoadingExpressCheckout(isLoading)
}
/>
{selectedPaymentMethod === EPaymentMethod.CREDIT_CARD && (
<CheckoutForm
confirmType={paymentType}
subscriptionReceiptId={paymentIntentId}
returnUrl={returnUrl}
/>
)}
</Elements>
)}
</div>
<SecurityPayments />
<p className={styles.address}>1123 Rimer Dr Moraga, California 94556</p>
</div>
<SecurityPayments />
<p className={styles.address}>500 N RAINBOW BLVD LAS VEGAS, NV 89107</p>
</div>
</>
);
}

View File

@ -8,6 +8,12 @@
color: #2f2e37;
}
.payment-modal.hide {
min-height: 0;
height: 0;
opacity: 0;
}
.title {
font-weight: 700;
font-size: 20px;
@ -31,6 +37,7 @@
.address {
margin-bottom: 24px;
text-transform: uppercase;
}
.payment-method {

View File

@ -1,9 +1,33 @@
import styles from "./styles.module.css";
import { TCanMakePaymentResult } from "@/components/PaymentPage/methods/StripeButton";
function PaymentButtons() {
return <div className={styles.container}>
<img src="/applepay.webp" alt="ApplePay" />
</div>;
interface IPaymentButtonsProps {
availableMethods: TCanMakePaymentResult;
}
const Image = ({ availableMethods }: IPaymentButtonsProps) => {
if (!availableMethods) return <></>;
if (availableMethods["applePay"]) {
return <img src="/applepay.webp" alt="ApplePay" />;
}
if (availableMethods["googlePay"]) {
return <img src="/google-pay-mark.png" alt="google" />;
}
if (availableMethods["amazon"]) {
return <img src="/amazon-pay-mark.png" alt="AmazonPay" />;
}
if (availableMethods["link"]) {
return <img src="/link-pay-mark.png" alt="LinkPay" />;
}
return <></>;
};
function PaymentButtons({ availableMethods }: IPaymentButtonsProps) {
if (!availableMethods) return <></>;
return (
<div className={styles.container}>
<Image availableMethods={availableMethods} />
</div>
);
}
export default PaymentButtons;

View File

@ -8,5 +8,5 @@
}
.container > img {
height: 16px;
height: 22px;
}

View File

@ -1,5 +1,7 @@
import { TCanMakePaymentResult } from "@/components/PaymentPage/methods/StripeButton";
import CreditCard from "@/components/ui/PaymentMethodsButtons/CreditCard";
import PaymentButtons from "@/components/ui/PaymentMethodsButtons/PaymentButtons";
import { AvailablePaymentMethods } from "@stripe/stripe-js";
export enum EPaymentMethod {
CREDIT_CARD = "card",
@ -11,13 +13,54 @@ export interface IPaymentMethod {
component: JSX.Element;
}
export const paymentMethods: IPaymentMethod[] = [
{
id: EPaymentMethod.PAYMENT_BUTTONS,
component: <PaymentButtons />,
},
{
id: EPaymentMethod.CREDIT_CARD,
component: <CreditCard />,
},
];
// export const paymentMethods: IPaymentMethod[] = [
// {
// id: EPaymentMethod.PAYMENT_BUTTONS,
// component: <PaymentButtons />,
// },
// {
// id: EPaymentMethod.CREDIT_CARD,
// component: <CreditCard />,
// },
// ];
export const checkExpressCheckoutStripeFormAvailable = (
availablePaymentMethods: undefined | AvailablePaymentMethods
) => {
if (!availablePaymentMethods) return false;
let result = false;
for (const key in availablePaymentMethods) {
if (availablePaymentMethods[key as keyof AvailablePaymentMethods]) {
result = true;
break;
}
}
return result;
};
export function paymentMethods(
availableMethods: TCanMakePaymentResult
): IPaymentMethod[] {
let methods = [
{
id: EPaymentMethod.PAYMENT_BUTTONS,
component: <PaymentButtons availableMethods={availableMethods} />,
},
{
id: EPaymentMethod.CREDIT_CARD,
component: <CreditCard />,
},
];
if (
!availableMethods ||
!checkExpressCheckoutStripeFormAvailable(
availableMethods as AvailablePaymentMethods
)
) {
methods = methods.filter(
(method) => method.id !== EPaymentMethod.PAYMENT_BUTTONS
);
}
return methods;
}

View File

@ -33,6 +33,7 @@ export const useAuthentication = () => {
partnerBirthPlace,
partnerBirthtime,
} = useSelector(selectors.selectQuestionnaire)
const { checked, dateOfCheck } = useSelector(selectors.selectPrivacyPolicy)
const birthdateFromForm = useSelector(selectors.selectBirthdate);
const birthtimeFromForm = useSelector(selectors.selectBirthtime);
@ -94,7 +95,9 @@ export const useAuthentication = () => {
birthplace: {
address: partnerBirthPlace,
},
}
},
sign: checked,
signDate: dateOfCheck
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
@ -108,6 +111,8 @@ export const useAuthentication = () => {
partnerName,
username,
birthtime,
checked,
dateOfCheck
]);
const authorization = useCallback(async (email: string, source: ESourceAuthorization) => {

View File

@ -0,0 +1,98 @@
import { IPaywallProduct } from "@/api/resources/Paywall";
import routes from "@/routes";
import { useElements, useStripe } from "@stripe/react-stripe-js";
import { CanMakePaymentResult, PaymentRequest } from "@stripe/stripe-js";
import { useEffect, useMemo, useState } from "react"
import { useNavigate } from "react-router-dom";
export type TCanMakePaymentResult = CanMakePaymentResult | null;
const getAmountFromProduct = (subPlan: IPaywallProduct) => {
if (subPlan.isTrial) {
return subPlan.trialPrice;
}
return subPlan.price;
};
interface IUseCanUseStripeButton {
activeProduct: IPaywallProduct | null;
client_secret: string;
subscriptionReceiptId?: string;
returnUrl?: string;
setCanMakePayment?: (canMakePayment: TCanMakePaymentResult) => void;
setCanMakePaymentLoading?: (loading: boolean) => void;
}
export const useCanUseStripeButton = ({
activeProduct,
client_secret,
subscriptionReceiptId,
returnUrl,
}: IUseCanUseStripeButton) => {
const stripe = useStripe();
const elements = useElements();
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest | null>(
null
);
const [availableMethods, setAvailableMethods] = useState<TCanMakePaymentResult>(null);
const navigate = useNavigate();
useEffect(() => {
if (!stripe || !elements || !activeProduct) {
return;
}
const pr = stripe.paymentRequest({
country: "US",
currency: "usd",
total: {
label: activeProduct.name || "Subscription",
amount: getAmountFromProduct(activeProduct),
},
requestPayerName: true,
requestPayerEmail: true,
});
pr.canMakePayment()
.then((result) => {
if (result) {
setPaymentRequest(pr)
setAvailableMethods(result)
}
})
pr.on("paymentmethod", async (e) => {
const { error: stripeError, paymentIntent } =
await stripe.confirmCardPayment(
client_secret,
{
payment_method: e.paymentMethod.id,
},
{ handleActions: false }
);
paymentIntent;
if (stripeError) {
// Show error to your customer (e.g., insufficient funds)
navigate(
`${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=failed`
);
return e.complete("fail");
}
navigate(
returnUrl ||
`${routes.client.paymentResult()}/${subscriptionReceiptId}/?redirect_status=succeeded`
);
e.complete("success");
});
}, [activeProduct, client_secret, elements, navigate, returnUrl, stripe, subscriptionReceiptId]);
return useMemo(() => ({
paymentRequest,
availableMethods
}), [
paymentRequest,
availableMethods
])
}

View File

@ -4,7 +4,7 @@ export default {
next: "Next",
date_of_birth: "What's your date of birth?",
privacy_text: "By continuing, you agree to our <eulaLink> and <privacyLink>. Have a question? Reach our support team <clickHere>",
eula: "Hint's EULA",
eula: "EULA",
here: 'here',
privacy_notice: 'Privacy Notice',
born_time_question: "What time were you born?",

View File

@ -32,6 +32,7 @@ import payment, {
actions as paymentActions,
selectActiveProduct,
selectIsDiscount,
selectStripeButton,
selectSubscriptionReceipt,
} from "./payment";
import subscriptionPlans, {
@ -69,6 +70,7 @@ import palmistry, {
selectPalmistryLines,
} from "./palmistry";
import { selectPaywallsIsMustUpdate, selectPaywalls } from "./paywalls";
import privacyPolicy, { actions as privacyPolicyActions, selectPrivacyPolicy } from "./privacyPolicy";
const preloadedState = loadStore();
export const actions = {
@ -88,6 +90,7 @@ export const actions = {
questionnaire: questionnaireActions,
userConfig: userConfigActions,
palmistry: palmistryActions,
privacyPolicy: privacyPolicyActions,
reset: createAction("reset"),
};
export const selectors = {
@ -120,6 +123,8 @@ export const selectors = {
selectPalmistryLines,
selectPaywalls,
selectPaywallsIsMustUpdate,
selectPrivacyPolicy,
selectStripeButton,
...formSelectors,
};
@ -140,6 +145,7 @@ export const reducer = combineReducers({
userConfig,
palmistry,
paywalls,
privacyPolicy
});
export type RootState = ReturnType<typeof reducer>;

View File

@ -1,13 +1,21 @@
import { IPaywallProduct } from "@/api/resources/Paywall";
import { SubscriptionReceipt } from "@/api/resources/UserSubscriptionReceipts";
import { TCanMakePaymentResult } from "@/hooks/payment/useCanUseStripeButton";
import { createSlice, createSelector } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
import { PaymentRequest } from "@stripe/stripe-js";
interface IStripeButton {
paymentRequest: PaymentRequest | null;
availableMethods: TCanMakePaymentResult;
}
interface IPayment {
selectedPrice: number | null;
isDiscount: boolean;
subscriptionReceipt: SubscriptionReceipt | null;
activeProduct: IPaywallProduct | null;
stripeButton: IStripeButton;
}
const initialState: IPayment = {
@ -15,6 +23,10 @@ const initialState: IPayment = {
isDiscount: false,
subscriptionReceipt: null,
activeProduct: null,
stripeButton: {
paymentRequest: null,
availableMethods: null,
}
};
const paymentSlice = createSlice({
@ -24,6 +36,9 @@ const paymentSlice = createSlice({
update(state, action: PayloadAction<Partial<IPayment>>) {
return { ...state, ...action.payload };
},
updateStripeButton(state, action: PayloadAction<IStripeButton>) {
return { ...state, stripeButton: action.payload };
},
},
extraReducers: (builder) => builder.addCase("reset", () => initialState),
});
@ -45,4 +60,8 @@ export const selectSubscriptionReceipt = createSelector(
(state: { payment: IPayment }) => state.payment.subscriptionReceipt,
(payment) => payment
);
export const selectStripeButton = createSelector(
(state: { payment: IPayment }) => state.payment.stripeButton,
(payment) => payment
);
export default paymentSlice.reducer;

View File

@ -0,0 +1,30 @@
import { createSlice, createSelector } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
interface IPrivacyPolicy {
checked: boolean
dateOfCheck: string
}
const initialState: IPrivacyPolicy = {
checked: false,
dateOfCheck: '',
}
const privacyPolicySlice = createSlice({
name: 'privacyPolicy',
initialState,
reducers: {
updateChecked(state, action: PayloadAction<boolean>) {
return { ...state, checked: action.payload, dateOfCheck: new Date().toISOString() }
},
},
extraReducers: (builder) => builder.addCase('reset', () => initialState),
})
export const { actions } = privacyPolicySlice
export const selectPrivacyPolicy = createSelector(
(state: { privacyPolicy: IPrivacyPolicy }) => state.privacyPolicy,
(privacyPolicy) => privacyPolicy
)
export default privacyPolicySlice.reducer

View File

@ -5,7 +5,7 @@ import { getClientLocale, getClientTimezone } from '../locales'
const initialState: User.User = {
id: undefined,
username: "I Am",
username: "",
email: '',
locale: getClientLocale(),
state: '',