Merge branch 'AW-86-policyMark' into 'develop'

AW-86-policyMark

See merge request witapp/aura-webapp!165
This commit is contained in:
Daniil Chemerkin 2024-06-11 17:40:00 +00:00
commit cacd28b395
13 changed files with 365 additions and 8 deletions

View File

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

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,12 @@
.container {
width: 100%;
display: flex;
flex-direction: row;
gap: 8px;
}
.text {
font-size: 14px;
line-height: 125%;
color: #515151;
}

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

@ -3,14 +3,16 @@ import Title from "@/components/Title";
import { Gender } from "@/data"; import { Gender } from "@/data";
import { EProductKeys } from "@/data/products"; import { EProductKeys } from "@/data/products";
import routes from "@/routes"; import routes from "@/routes";
import { actions } from "@/store"; import { actions, selectors } from "@/store";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useDispatch } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import BackgroundTopBlob from "../../ui/BackgroundTopBlob"; import BackgroundTopBlob from "../../ui/BackgroundTopBlob";
import { useDynamicSize } from "@/hooks/useDynamicSize"; import { useDynamicSize } from "@/hooks/useDynamicSize";
import Header from "../../components/Header"; import Header from "../../components/Header";
import { genders } from "../../data/genders"; import { genders } from "../../data/genders";
import PrivacyPolicy from "../../components/PrivacyPolicy";
import Toast from "../../components/Toast";
interface IGenderPageProps { interface IGenderPageProps {
productKey?: EProductKeys; productKey?: EProductKeys;
@ -22,16 +24,26 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element {
const { targetId } = useParams(); const { targetId } = useParams();
const { width: pageWidth, elementRef: pageRef } = useDynamicSize({}); const { width: pageWidth, elementRef: pageRef } = useDynamicSize({});
const [selectedGender, setSelectedGender] = useState<Gender | null>(null); const [selectedGender, setSelectedGender] = useState<Gender | null>(null);
const { checked: privacyPolicyChecked } = useSelector(
selectors.selectPrivacyPolicy
);
useEffect(() => { useEffect(() => {
const isShowTryApp = targetId === "i"; const isShowTryApp = targetId === "i";
dispatch(actions.userConfig.addIsShowTryApp(isShowTryApp)); dispatch(actions.userConfig.addIsShowTryApp(isShowTryApp));
}, [dispatch, targetId]); }, [dispatch, targetId]);
const selectGender = async (gender: Gender) => { useEffect(() => {
setSelectedGender(gender); 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)); 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"]) { if (productKey === EProductKeys["moons.pdf.aura"]) {
return navigate(routes.client.epeBirthdate()); return navigate(routes.client.epeBirthdate());
} }
@ -41,6 +53,18 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element {
navigate(`/v1/questionnaire/profile/flowChoice`); 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 getButtonBGColor = (gender: Gender): string => {
const { colorAssociation } = gender; const { colorAssociation } = gender;
if (Array.isArray(colorAssociation)) { if (Array.isArray(colorAssociation)) {
@ -114,6 +138,12 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element {
</div> </div>
))} ))}
</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> </section>
); );
} }

View File

@ -106,18 +106,43 @@
.gender--selected { .gender--selected {
transform: scale(1.1); transform: scale(1.1);
/* animation: gender-click 1s linear; */ /* animation: gender-click 1.4s linear; */
} }
.gender--selected .gender__slide-element { .gender--selected .gender__slide-element {
left: calc(100% - 27px - 10px); 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 { @keyframes gender-click {
0% { 0% {
transform: scale(1); transform: scale(1);
} }
100% { 50% {
transform: scale(1.1); 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

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

View File

@ -69,6 +69,7 @@ import palmistry, {
selectPalmistryLines, selectPalmistryLines,
} from "./palmistry"; } from "./palmistry";
import { selectPaywallsIsMustUpdate, selectPaywalls } from "./paywalls"; import { selectPaywallsIsMustUpdate, selectPaywalls } from "./paywalls";
import privacyPolicy, { actions as privacyPolicyActions, selectPrivacyPolicy } from "./privacyPolicy";
const preloadedState = loadStore(); const preloadedState = loadStore();
export const actions = { export const actions = {
@ -88,6 +89,7 @@ export const actions = {
questionnaire: questionnaireActions, questionnaire: questionnaireActions,
userConfig: userConfigActions, userConfig: userConfigActions,
palmistry: palmistryActions, palmistry: palmistryActions,
privacyPolicy: privacyPolicyActions,
reset: createAction("reset"), reset: createAction("reset"),
}; };
export const selectors = { export const selectors = {
@ -120,6 +122,7 @@ export const selectors = {
selectPalmistryLines, selectPalmistryLines,
selectPaywalls, selectPaywalls,
selectPaywallsIsMustUpdate, selectPaywallsIsMustUpdate,
selectPrivacyPolicy,
...formSelectors, ...formSelectors,
}; };
@ -140,6 +143,7 @@ export const reducer = combineReducers({
userConfig, userConfig,
palmistry, palmistry,
paywalls, paywalls,
privacyPolicy
}); });
export type RootState = ReturnType<typeof reducer>; export type RootState = ReturnType<typeof 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