Skip to content

Commit

Permalink
Merge pull request #26 from process/signin-process
Browse files Browse the repository at this point in the history
Process/signin process
  • Loading branch information
cjhih456 authored Feb 3, 2025
2 parents 2ed058c + 2da53b6 commit 959f204
Show file tree
Hide file tree
Showing 21 changed files with 153 additions and 42 deletions.
3 changes: 2 additions & 1 deletion public/locales/en/page-home.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
},
"emailForm": {
"label": "Email address",
"button": "Get Started"
"button": "Get Started",
"finishRegist": "Finish Sign-Up"
}
}
3 changes: 2 additions & 1 deletion public/locales/kr/page-home.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
},
"emailForm": {
"label": "이메일 주소",
"button": "시작하기"
"button": "시작하기",
"finishRegist": "가입 마무리하기"
}
}
56 changes: 38 additions & 18 deletions src/components/pages/Home/component/EmailSubmitForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { useMutation } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import DarkTextInput from '@/components/ui/Form/DarkTextInput';
import TextInput from '@/components/ui/Form/TextInput';
import ConditionalRender from '@/components/ui/utils/ConditionalRender';
import useJWTs from '@/hooks/account/useJWTs';
import { EmailCheckApi } from '@/lib/network/account/EmailCheckApi';
import { pattern } from '@/lib/validators';
import { EmailFormRowLayoutCss, EmailFormSubmitBtnCss } from '../styles/EmailSubmitFormCss';
import { EmailFormFinishSignupBtnCss, EmailFormRowLayoutCss, EmailFormSubmitBtnCss } from '../styles/EmailSubmitFormCss';
import { HeroDescrpition2 } from '../styles/HeroSection';

interface FormData {
Expand All @@ -21,6 +23,8 @@ export default function EmailSubmitForm() {
shouldUseNativeValidation: false,
})

const { hasLogin } = useJWTs()

const { invalid, error, isTouched } = getFieldState('email')
const { mutate, isPending } = useMutation({
mutationFn: EmailCheckApi,
Expand All @@ -36,27 +40,43 @@ export default function EmailSubmitForm() {
sessionStorage.setItem('sign-tryed-email', obj.email)
mutate(obj.email)
}
function gotoRegistMembershipWithPaymentMethod() {
router.push('/signup')
}
return <form onSubmit={handleSubmit(submitAction)}>
<HeroDescrpition2>
{t('page-home:section1.desc2')}
</HeroDescrpition2>
<div css={EmailFormRowLayoutCss}>
<DarkTextInput
isValid={isTouched && !invalid}
error={error?.message}
placeholder={t('page-home:emailForm.label')}
{...register('email', {
required: t('form.email.error.required'),
pattern: {
value: pattern.email,
message: t('common:form.email.error.pattern')
}
})}
></DarkTextInput>
<button type="submit" css={EmailFormSubmitBtnCss}>
{/* TODO: add spinner */}
{isPending ? <div></div> : t('page-home:emailForm.button')}
</button>
<ConditionalRender.Boolean
condition={hasLogin}
render={{
true: <button
onClick={gotoRegistMembershipWithPaymentMethod}
css={EmailFormFinishSignupBtnCss}
>
{t('page-home:emailForm.finishRegist')}
</button>,
false: <>
<TextInput.Dark
isValid={isTouched && !invalid}
error={error?.message}
label={t('page-home:emailForm.label')}
{...register('email', {
required: t('form.email.error.required'),
pattern: {
value: pattern.email,
message: t('common:form.email.error.pattern')
}
})}
/>
<button type="submit" css={EmailFormSubmitBtnCss}>
{/* TODO: add spinner */}
{isPending ? <div></div> : t('page-home:emailForm.button')}
</button>
</>
}}
/>
</div>
</form>
}
11 changes: 10 additions & 1 deletion src/components/pages/Home/styles/EmailSubmitFormCss.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export const EmailFormRowLayoutCss = css([{
paddingTop: '1.625rem',
display: 'flex',
rowGap: '1rem',
columnGap: '0.5rem'
columnGap: '0.5rem',
justifyContent: 'center',
}, MediaPoint({
alignItems: ['center', 'start'],
flexDirection: ['column', 'row'],
Expand All @@ -19,4 +20,12 @@ export const EmailFormSubmitBtnCss = css([DefaultButtonCss, RedButtonCss.color,
minHeight: '3.5rem'
}, MediaPoint({
padding: ['0.5rem 1rem', '0.75rem 1.5rem']
})])

export const EmailFormFinishSignupBtnCss = css([DefaultButtonCss, RedButtonCss.color, RedButtonCss.interaction.dark, {
fontSize: '1.125rem',
fontWeight: 700,
minHeight: '3.5rem'
}, MediaPoint({
padding: ['0.5rem 1rem', '0.75rem 1.5rem']
})])
31 changes: 28 additions & 3 deletions src/components/pages/Signin/hooks/useSigninWIthPasswordMutate.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,50 @@
import type { SigninResponseType } from '@/lib/network/types/account';
import type { ErrorResponse } from '@/lib/network/types/error';
import { useMutation } from '@tanstack/react-query'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useAtom } from 'jotai'
import { useRouter } from 'next/router';
import { MY_INFO_QUERY_KEY } from '@/hooks/Query/keys/account';
import useGetSigninAccountInfo from '@/hooks/Query/useGetLoginAccountInfo';
import { SigninApi } from '@/lib/network/account/SigninApi'
import { ErrorCode } from '@/mocks/middleware/ErrorHandler';
import { currentProfileAtom } from '@/state/Profile';
import { accessTokenAtom, refreshTokenAtom } from '@/state/Token'

export default function useSigninWIthPasswordMutate() {
const router = useRouter()
const queryClient = useQueryClient()
const { data: accountInfo } = useGetSigninAccountInfo({
enabled: false
})
const [, setCurrentProfile] = useAtom(currentProfileAtom)
const [, setAccessToken] = useAtom(accessTokenAtom)
const [, setRefreshToken] = useAtom(refreshTokenAtom)
const { mutate: signinMutate } = useMutation({
mutationFn: SigninApi,
onSuccess: loginSuccessAction,
onError: loginFailedAction
})
function loginSuccessAction(data: SigninResponseType) {
async function loginSuccessAction(data: SigninResponseType) {
setAccessToken(data.accessToken)
setRefreshToken(data.refreshToken)
router.push('/')
queryClient.invalidateQueries({
queryKey: MY_INFO_QUERY_KEY,
refetchType: 'inactive',
})
if (!accountInfo?.accountInfo.membership) {
if (!accountInfo?.accountInfo.profiles?.length) {
router.push('/firstProfile')
} else if (accountInfo.accountInfo.profiles.length === 1) {
if (accountInfo.accountInfo.profiles[0]) {
setCurrentProfile(accountInfo.accountInfo.profiles[0])
}
router.push('/browse')
} else {
router.push('/selectProfile')
}
} else {
router.push('/')
}
}
function loginFailedAction(error: Error) {
const errorState: ErrorResponse = JSON.parse(error.message)
Expand Down
13 changes: 11 additions & 2 deletions src/components/pages/Signup/platform.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { NextPageWithLayout } from '@/pages/_app';
import { useAtom } from 'jotai';
import Link from 'next/link';
import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react';
import SignupLayout from '@/components/layout/SignupLayout';
import ConditionalRender from '@/components/ui/utils/ConditionalRender';
import useJWTs from '@/hooks/account/useJWTs';
import useWindowResize from '@/hooks/useWindowResize';
import { signupMembershipTier } from '@/state/Signup';
import StepHeader from './component/StepHeader';
import PlatformDetailLarge from './component/platform/PlatfomDetailLarge';
import PlatformDetailSlim from './component/platform/PlatfomDetailSlim';
Expand All @@ -14,6 +17,8 @@ import { SignupPlatformContentCss, SignupPlatformContentLargeCss } from './style
const PlatformPage: NextPageWithLayout = () => {
const { t } = useTranslation(['page-signup'])

const { hasLogin } = useJWTs()
const [, setMembershipTier] = useAtom(signupMembershipTier)
// on resize display width > over 1050px change contents as full width mode
const {
isLarge
Expand All @@ -24,9 +29,13 @@ const PlatformPage: NextPageWithLayout = () => {
useEffect(() => {
setIsClient(true)
}, [])

const goNextAction = () => {
setMembershipTier(selectedType)
}
return <>
<div css={[SignupPlatformContentCss, isLarge ? SignupPlatformContentLargeCss : {}]}>
<StepHeader css={{ marginBottom: '.5rem' }} step={1} title={t('page-signup:platform.title')} />
<StepHeader css={{ marginBottom: '.5rem' }} step={2} title={t('page-signup:platform.title')} />
<ConditionalRender.Boolean
condition={isClient}
render={{
Expand All @@ -41,7 +50,7 @@ const PlatformPage: NextPageWithLayout = () => {
/>
</div>
<div css={[SignupMainContentCss, { marginTop: '24px' }]}>
<Link css={SignupMainNextButtonCss} href="/signup/registration">
<Link css={SignupMainNextButtonCss} href={hasLogin ? '/signup/payment/regist/' : '/signup/registration'} onClick={goNextAction}>
Next
</Link>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/pages/Signup/registration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const RegistrationPage: NextPageWithLayout = () => {
<div css={SignupMainContentCss}>
<div css={[RegistrationLogoImageCss, StepLogoPositionCss]}>
</div>
<StepHeader title={t('page-signup:registration.title')} step={3} />
<StepHeader title={t('page-signup:registration.title')} step={2} />
<div>{t('page-signup:registration.desc')}</div>
</div>
<div css={[SignupMainContentCss, { marginTop: '24px' }]}>
Expand Down
6 changes: 2 additions & 4 deletions src/components/ui/Button/SignInOutBtn.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { AnchorHTMLAttributes } from 'react';
import Link from 'next/link';
import { useMemo } from 'react';
import ConditionalRender from '@/components/ui/utils/ConditionalRender';
import useJWTs from '@/hooks/account/useJWTs';

Expand All @@ -10,10 +9,9 @@ interface SignInOutBtnProps {
}

export default function SignInOutBtn({ signInText, signOutText, ...props }: SignInOutBtnProps & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'onClick' | 'href'> & CssProps) {
const { accessToken, removeJWT } = useJWTs()
const hasSignined = useMemo(() => Boolean(accessToken), [accessToken])
const { removeJWT, hasLogin } = useJWTs()
return <ConditionalRender.Boolean
condition={hasSignined}
condition={hasLogin}
render={{
true: <Link {...props} onClick={removeJWT} href="/signout">
{signOutText}
Expand Down
3 changes: 3 additions & 0 deletions src/hooks/Query/keys/account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { QueryKey } from '@tanstack/react-query';

export const MY_INFO_QUERY_KEY: QueryKey = ['account-my-info']
17 changes: 17 additions & 0 deletions src/hooks/Query/useGetLoginAccountInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { MyInfoResponseType } from '@/lib/network/types/account';
import { useQuery } from '@tanstack/react-query';
import { GetAccountInfoApi } from '@/lib/network/account/GetAccountInfoApi';
import { MY_INFO_QUERY_KEY } from './keys/account';

export default function useGetSigninAccountInfo(options: {
enabled: boolean
}) {
return useQuery<MyInfoResponseType>({
queryKey: MY_INFO_QUERY_KEY,
queryFn: async () => {
const result = await GetAccountInfoApi()
return result
},
...options
})
}
3 changes: 3 additions & 0 deletions src/hooks/account/useJWTs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useQueryClient } from '@tanstack/react-query';
import { useAtom } from 'jotai';
import { useMemo } from 'react';
import { accessTokenAtom, refreshTokenAtom } from '@/state/Token';

interface UpdateJWTParams {
Expand All @@ -11,13 +12,15 @@ export default function useJWTs() {
const queryClient = useQueryClient()
const [accessToken, setAccessToken] = useAtom(accessTokenAtom)
const [, setRefreshToken] = useAtom(refreshTokenAtom)
const hasLogin = useMemo(() => Boolean(accessToken), [accessToken])
const updateJWT = ({ accessToken, refreshToken }: UpdateJWTParams) => {
setAccessToken(accessToken)
setRefreshToken(refreshToken)
queryClient.clear()
}
const removeJWT = () => updateJWT({ accessToken: '', refreshToken: '' })
return {
hasLogin,
accessToken,
updateJWT,
removeJWT
Expand Down
7 changes: 7 additions & 0 deletions src/lib/network/account/GetAccountInfoApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { MyInfoResponseType } from '../types/account';
import api from '..'

export const GetAccountInfoApi = async () => {
const result = await api.get<MyInfoResponseType>('account/me').json()
return result
}
4 changes: 2 additions & 2 deletions src/lib/network/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const baseApi = ky.extend({
beforeRequest: [
(request) => {
if (typeof window !== 'undefined') {
const token = localStorage.getItem(ACCESS_TOKEN_KEY)
const token = localStorage.getItem(ACCESS_TOKEN_KEY)?.replace(/"/g, '')
if (token) {
request.headers.set('Authorization', `Bearer ${token}`)
}
Expand All @@ -40,7 +40,7 @@ const api = baseApi.extend({
hooks: {
beforeRetry: [
async ({ request }) => {
const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY)
const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY)?.replace(/"/g, '')
if (!refreshToken) return ky.stop
const tokens = await baseApi.post('account/refresh', {
json: {
Expand Down
6 changes: 5 additions & 1 deletion src/lib/network/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export const ACCESS_TOKEN_KEY = 'access-token'
export const REFRESH_TOKEN_KEY = 'refresh-token'

export const SIGNIN_EMAIL_OR_PHONE = 'signin-emailOrPhone'
export const SIGNIN_EMAIL_OR_PHONE = 'signin-emailOrPhone'

export const SIGNIN_CURRENT_PROFILE = 'profile-current-position'

export const SIGNUP_MEMBERSHIP_TIER = 'signup-membership-tier'
6 changes: 3 additions & 3 deletions src/mocks/middleware/token.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { JWTPayload } from 'jose';
import type { DefaultBodyType, HttpResponseResolver, PathParams } from 'msw';
import { jwtDecrypt, jwtVerify, SignJWT } from 'jose'
import { jwtVerify, SignJWT } from 'jose'
import ErrorException from '../type/ErrorResponse';
import { ErrorCode } from './ErrorHandler';

Expand Down Expand Up @@ -73,11 +73,11 @@ export function withAuth<RequestBodyType extends DefaultBodyType, ResponseBodyTy
export async function parseAuth(header: Headers) {
const parsedHeader = String(header.get('Authorization') || ' ').split(' ');
try {
const result = await jwtDecrypt<JwtPayloadType>(parsedHeader[1], secret)
const result = await jwtVerify<JwtPayloadType>(parsedHeader[1], secret)
if (!result) throw new Error()
return result
} catch {
throw new ErrorException('Token Verification failed, Please check token', ErrorCode.AUTH_EXPIRED)
throw new ErrorException('Token Parse failed, Please check token', ErrorCode.AUTH_EXPIRED)
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/state/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import { atomWithStorage } from 'jotai/utils';
import { SIGNIN_EMAIL_OR_PHONE } from '@/lib/network/utils';
import { JotaiSessionStorage } from './util/Storage';

export const signinEmailOrPhoneAtom = atomWithStorage(SIGNIN_EMAIL_OR_PHONE, '', JotaiSessionStorage)
export const signinEmailOrPhoneAtom = atomWithStorage(SIGNIN_EMAIL_OR_PHONE, '', JotaiSessionStorage<string>())
6 changes: 6 additions & 0 deletions src/state/Profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { atomWithStorage } from 'jotai/utils';
import { SIGNIN_CURRENT_PROFILE } from '@/lib/network/utils';
import { JotaiSessionStorage } from './util/Storage';

// TODO: with sessionStorage
export const currentProfileAtom = atomWithStorage(SIGNIN_CURRENT_PROFILE, undefined, JotaiSessionStorage<Profile | undefined>())
5 changes: 5 additions & 0 deletions src/state/Signup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { atomWithStorage } from 'jotai/utils';
import { SIGNUP_MEMBERSHIP_TIER } from '@/lib/network/utils';
import { JotaiSessionStorage } from './util/Storage';

export const signupMembershipTier = atomWithStorage(SIGNUP_MEMBERSHIP_TIER, undefined, JotaiSessionStorage<MembershipPlanTier | undefined>())
4 changes: 2 additions & 2 deletions src/state/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { atomWithStorage } from 'jotai/utils';
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from '@/lib/network/utils';
import { JotaiLocalStorage } from './util/Storage';

export const accessTokenAtom = atomWithStorage(ACCESS_TOKEN_KEY, '', JotaiLocalStorage)
export const refreshTokenAtom = atomWithStorage(REFRESH_TOKEN_KEY, '', JotaiLocalStorage)
export const accessTokenAtom = atomWithStorage(ACCESS_TOKEN_KEY, '', JotaiLocalStorage<string>())
export const refreshTokenAtom = atomWithStorage(REFRESH_TOKEN_KEY, '', JotaiLocalStorage<string>())
Loading

0 comments on commit 959f204

Please sign in to comment.