/* eslint-disable no-shadow */ /* eslint-disable complexity */ (() => { const { myapp, ShopbySkin } = window; if (!(myapp && ShopbySkin) || !(myapp.helpers.isMyApp() || myapp.helpers.isOpenedAsCustomTab())) { return; } const isMoveToSignIn = (url) => { const path = new URL(url, location.origin).pathname; return path.includes(ShopbySkin.constants.PAGE.SIGN_IN); }; const handleCustomTabsClosed = () => { if (document.visibilityState !== 'visible') { return; } const schemeLink = sessionStorage.getItem('CUSTOM_SCHEME'); const authData = sessionStorage.getItem('AUTH_DATA'); if ((schemeLink === 'customtabsclose://' || schemeLink === 'auth://') && authData) { const { accessToken, expiresIn, refreshToken, refreshTokenExpiresIn, isDormantMember, isInitialSync } = JSON.parse(authData); const expiryData = ShopbySkin.utils.getTokenExpiryConfig({ expires: expiresIn, refreshTokenExpiresIn, }); if (isInitialSync) { myapp.handler.send({ key: myapp.constants.SIGN_UP_COMPLETED, }); } if (schemeLink === 'customtabsclose://' && isDormantMember) { ShopbySkin.utils.dormancyAuth.set({ value: accessToken, expires: expiresIn, }); ShopbySkin.utils.refreshAuth.set({ value: refreshToken, expires: refreshTokenExpiresIn, }); } else if (schemeLink === 'customtabsclose://') { ShopbySkin.utils.memberAuth.set({ value: accessToken, ...expiryData, }); } else if (schemeLink === 'auth://') { ShopbySkin.utils.memberAuth.set({ value: accessToken, ...expiryData, }); ShopbySkin.utils.refreshAuth.set({ value: refreshToken, expires: refreshTokenExpiresIn, }); ShopbySkin.utils.setLocalStorage('MYAPP_SIGN_IN', 'true'); } sessionStorage.removeItem('CUSTOM_SCHEME'); sessionStorage.removeItem('AUTH_DATA'); } }; const handleRejectionProxy = (callback) => new Proxy(callback, { apply(target, thisArg, argArr) { const isRefreshTokenExpired = argArr[0].reason?.error?.code === ShopbySkin.constants.CLIENT_ERROR.EXPIRED_REFRESH_TOKEN; if (isRefreshTokenExpired) { myapp.handler.send({ key: myapp.constants.REFRESH_TOKEN_EXPIRED, option: { returnUrl: location.href, }, }); return; } target?.apply?.(thisArg, argArr); }, }); /** * 퀵 로그인 사용 안 함 일 때, LOGINVIEW 브릿지 이벤트 발생 시 로그인 페이지(/pages/sign-in/sign-in.html)로 이동됩니다. * 이때, LOGINVIEW 브릿지 호출 시 함께 전달한 `returnUrl`이 로그인 페이지의 URL 쿼리 파라미터 `next`로 전달됩니다.(고도몰과 스펙을 맞추기위해 next라는 이름의 파라미터를 사용하는 것입니다.) * 로그인 페이지에서 `next`라는 파라미터가 있다면, 해당 파라미터를 `from`으로 바꿔치기 하기 위한 함수입니다. */ const replaceNextToFrom = () => { // 1. 현재 URL의 쿼리 파라미터를 가져온다 const url = new URL(window.location.href); const path = url.pathname; const params = url.searchParams; // 2. 'next' 파라미터가 있으면 'from'으로 바꿔치기 if (path.startsWith(ShopbySkin.constants.PAGE.SIGN_IN) && params.has('next')) { const value = params.get('next'); params['delete']('next'); params.set('from', value); // 3. 새 쿼리 스트링을 적용한 새 URL 생성 url.search = params.toString(); // 4. history.replaceState로 주소만 바꿔치기 (새로고침 없음) history.replaceState(null, '', url.toString()); } }; const isSignedInProxy = (callback) => new Proxy(callback, { apply(target, thisArg, argArr) { const hasSignedInForMyapp = ShopbySkin.utils.getLocalStorage('MYAPP_SIGN_IN') === 'true'; const isSignedIn = target.apply(thisArg, argArr); if (!hasSignedInForMyapp) { return isSignedIn; } if (!isSignedIn) { myapp.handler.send({ key: myapp.constants.REFRESH_TOKEN_EXPIRED, option: { returnUrl: location.href, }, }); const preventMoveTo = (callback) => new Proxy(callback, { apply: () => null }); ShopbySkin.utils.moveTo.applyMyappProxy(preventMoveTo); } return isSignedIn; }, }); const navigateProxySignIn = (callback) => new Proxy(callback, { apply(target, thisArg, argArr) { const [{ url }] = argArr; const urlObj = new URL(url, location.origin); const fromUrl = urlObj.searchParams.get('from'); if (isMoveToSignIn(url)) { const pathInfo = { previousPath: location.href, nextPath: fromUrl, }; let returnUrl = fromUrl ?? location.href; if (returnUrl.includes(ShopbySkin.constants.PAGE.SIGN_UP_METHOD)) { returnUrl = `${location.origin}${ShopbySkin.constants.PAGE.MAIN}`; } ShopbySkin.utils.setLocalStorage('pathInfo', JSON.stringify(pathInfo)); myapp.handler.send({ key: myapp.constants.LOGINVIEW, // /pages/sign-in/sign-in.html option: { returnUrl, }, }); return; } const isMoveToAdultCertificationPage = urlObj.pathname?.includes(ShopbySkin.constants.PAGE.ADULT_CERTIFICATION); if (isMoveToAdultCertificationPage) { target.apply(thisArg, [ { ...argArr[0], url: urlObj .toString() .replace(ShopbySkin.constants.PAGE.ADULT_CERTIFICATION, myapp.constants.ADULT_CERTIFICATION_PAGE), }, ]); return; } target.apply(thisArg, argArr); }, }); const historyReplaceProxy = (callback) => new Proxy(callback, { apply(target, thisArg, argArr) { const [{ url, state }] = argArr; if (state?.isIntroPage) { target.apply(thisArg, [ { state, url: url.replace('/pages/intro/', '/app/'), }, ]); return; } target.apply(thisArg, argArr); }, }); const openIdUrlProxy = (callback) => new Proxy(callback, { apply(_, __, argArr) { const [{ provider, previousPath, nextPath }] = argArr; const openIdSignInUrl = `${location.origin}${myapp.constants.OPEN_ID_SIGN_IN_PAGE}?provider=${provider}&previous=${previousPath}&next=${nextPath}`; // NOTE : OS 별 정책에 따라, SNS 인증 시 커스텀 탭을 열어야 하는 경우가 있음. 예) IOS: 구글 / AOS: 페이스북, 구글 const shouldOpenCustomTab = myapp.helpers.shouldUseCustomScheme(provider ?? ''); if (shouldOpenCustomTab) { const packageName = myapp.helpers.getPackageName(); const links = JSON.stringify({ url: encodeURIComponent(openIdSignInUrl), }); const scheme = `${packageName}.customtabs://${provider}?links=${links}`; myapp.handler.scheme(scheme); return; } ShopbySkin.utils.moveTo({ url: openIdSignInUrl, }); }, }); const slideMenuProxy = (callback) => new Proxy(callback, { apply(target, thisArg, argArr) { const eventTarget = argArr[0].target; const action = eventTarget?.closest('[shopby-action]')?.getAttribute('shopby-action'); if (action === 'SIGN_OUT') { myapp.handler.send({ key: myapp.constants.LOGOUT, option: { returnUrl: location.href, }, }); return; } if (action === 'GO_NEXT' && isMoveToSignIn(eventTarget.href)) { argArr[0].stopPropagation(); } target.apply(thisArg, argArr); }, }); const withdrawalProxy = (callback) => { ShopbySkin.EventManager.on('MODAL_ALERT_OPEN', (payload) => { if (myapp.helpers.isMyApp() && payload.noticeType === 'SUCCESS') { myapp.handler.send({ key: myapp.constants.INIT_LOGIN_INFO, }); ShopbySkin.utils.removeLocalStorage('MYAPP_SIGN_IN'); } }); return openIdUrlProxy(callback); }; const signInFormProxy = (callback) => new Proxy(callback, { async apply(target, thisArg, argArr) { const { data: signInData } = await target.apply(thisArg, argArr); if (signInData.dormantMemberResponse) { return { data: signInData }; } const { data: profile } = await ShopbySkin.actions.queryProfile(); myapp.handler.send({ key: myapp.constants.LOGIN, option: { memberId: profile.memberId, memberNo: profile.memberNo, memberName: profile.memberName, nickName: profile.nickname, solutionAuthenticationKey: ShopbySkin.utils.memberAuth.get(), }, }); ShopbySkin.utils.setLocalStorage('MYAPP_SIGN_IN', 'true'); return { data: signInData }; }, }); /** * SIGN_UP_COMPLETED : 회원가입 시 앱 단에서 보유중인 로그인 정보를 초기화 시키기 위해 사용 * - 예 : 로그인 -> 생체인증 on -> 앱 종료후 재실행(이때 로그아웃 되어있으나, 생체인증 정보는 남아있는 상태) * -> 회원가입 후 로그인 시 앱에 남아있던 생체인증 정보로 로그인을 하는 상황이 발생 -> 이를 방지하기 위해 사용 */ const signUpFormProxy = (callback) => new Proxy(callback, { async apply(target, thisArg, argArr) { const { data } = await target.apply(thisArg, argArr); myapp.handler.send({ key: myapp.constants.SIGN_UP_COMPLETED, }); return { data }; }, }); /** * 퀵 로그인 사용 안 함 && (일반 회원)휴면 회원인 경우 * 휴면 해제 레이어 모달을 통해 "휴면 해제하기" 버튼 클릭 시 `LOGIN` 브릿지 이벤트를 보내는 프록시 함수 */ const dormantSignInLayerModalProxy = (callback) => new Proxy(callback, { async apply(target, thisArg, argArr) { await target.apply(thisArg, argArr); const { data: profile } = await ShopbySkin.actions.queryProfile(); myapp.handler.send({ key: myapp.constants.LOGIN, option: { memberId: profile.memberId, memberNo: profile.memberNo, memberName: profile.memberName, nickName: profile.nickname, solutionAuthenticationKey: ShopbySkin.utils.memberAuth.get(), }, }); }, }); const authCallbackSignInProxy = (callback) => new Proxy(callback, { async apply(target, thisArg, argArr) { const res = await target.apply(thisArg, argArr); const { profile, accessToken, snsProvider, isDormantMember, isInactiveMember, isSecurityAuth, isMyPage, isInitialSync, } = res; if (isInitialSync) { myapp.handler.send({ key: myapp.constants.SIGN_UP_COMPLETED, }); } /** * 휴면 회원 또는 비활성 회원이거나 개인정보 조회용 소셜 인증 과정에서는 `LOGIN` 브릿지 이벤트를 보내지 않고, * 각 케이스마다 스킨에서 정의한 과정을 거치도록 함. */ if (isDormantMember || isInactiveMember || isSecurityAuth || isMyPage) { return res; } myapp.handler.send({ key: myapp.constants.LOGIN, option: { memberId: profile?.memberId, memberNo: profile?.memberNo, memberName: profile?.memberName, nickName: profile?.nickname, solutionAuthenticationKey: accessToken, snsProvider, }, }); ShopbySkin.utils.setLocalStorage('MYAPP_SIGN_IN', 'true'); return res; }, }); const customTabsAuthCallbackSignInProxy = (callback) => new Proxy(callback, { async apply(target, thisArg, argArr) { // ======= [1] 에러 처리 ======= const searchParams = new URLSearchParams(decodeURIComponent(location.search)); const errorCode = searchParams.get('error') || searchParams.get('error_code'); if (errorCode) { const provider = ShopbySkin.utils.getLocalStorage('provider').replace('ncp_', ''); const schemeLink = new URL(`${myapp.helpers.getPackageName()}.customtabsclose://${provider}`); myapp.handler.scheme(schemeLink.toString()); return {}; } // ======= [2] 인증 성공 시 처리 ======= const res = await target.apply(thisArg, argArr); const { profile, nextPath, snsProvider, isDormantMember, isInactiveMember, isSecurityAuth, isMyPage, isInitialSync, accessToken, expiresIn, refreshToken, refreshTokenExpiresIn, } = res; const signInUrl = new URL(ShopbySkin.constants.PAGE.SIGN_IN, location.origin).toString(); const signUpOpenIdUrl = new URL(ShopbySkin.constants.PAGE.SIGN_UP_OPEN_ID, location.origin).toString(); const nextUrl = nextPath ? new URL(nextPath, location.origin).toString() : ''; const schemeBase = `${myapp.helpers.getPackageName()}.customtabsclose://${snsProvider}`; const schemeLink = new URL(schemeBase); // ======= [3-1] 휴면 회원 처리 ======= if (isDormantMember) { schemeLink.searchParams.set('next', signInUrl); schemeLink.searchParams.set( 'localStorages', JSON.stringify({ [ShopbySkin.utils.convertName('OPEN_ID_DORMANT_CALLBACK')]: ShopbySkin.utils.getLocalStorage('OPEN_ID_DORMANT_CALLBACK'), }) ); schemeLink.searchParams.set( 'sessionStorages', JSON.stringify({ CUSTOM_SCHEME: 'customtabsclose://', AUTH_DATA: { accessToken, expiresIn, refreshToken, refreshTokenExpiresIn, isDormantMember, }, }) ); myapp.handler.scheme(schemeLink.toString()); return {}; } // ======= [3-2] memberStatus가 ACTIVE가 아닌 회원 처리(예: 가입대기 회원) ======= if (isInactiveMember) { schemeLink.searchParams.set('next', signUpOpenIdUrl); schemeLink.searchParams.set( 'localStorages', JSON.stringify({ [ShopbySkin.utils.convertName('OPEN_ID_CALLBACK')]: ShopbySkin.utils.getLocalStorage('OPEN_ID_CALLBACK'), }) ); schemeLink.searchParams.set( 'sessionStorages', JSON.stringify({ CUSTOM_SCHEME: 'customtabsclose://', AUTH_DATA: { isInitialSync, accessToken, expiresIn, refreshToken, refreshTokenExpiresIn, }, }) ); myapp.handler.scheme(schemeLink.toString()); return {}; } // ======= [3-3] 마이페이지 개인정보조회용(논마스킹) 소셜 인증 처리 ======= if (isSecurityAuth) { schemeLink.searchParams.set('next', nextUrl); schemeLink.searchParams.set( 'sessionStorages', JSON.stringify({ MY_PAGE_TOKEN: sessionStorage.getItem('MY_PAGE_TOKEN'), CUSTOM_SCHEME: 'customtabsclose://', AUTH_DATA: { accessToken, expiresIn, refreshToken, refreshTokenExpiresIn, }, }) ); myapp.handler.scheme(schemeLink.toString()); return {}; } // ======= [3-4] 마이페이지 소셜 인증 처리 ======= if (isMyPage) { schemeLink.searchParams.set('next', nextUrl); schemeLink.searchParams.set( 'sessionStorages', JSON.stringify({ SUCCESS_PASSWORD_AUTHENTICATION: sessionStorage.getItem('SUCCESS_PASSWORD_AUTHENTICATION'), }) ); myapp.handler.scheme(schemeLink.toString()); return {}; } // ======= [3-5] 로그인 처리 ======= const authSchemeData = JSON.stringify({ CUSTOM_SCHEME: 'auth://', AUTH_DATA: { accessToken, expiresIn, refreshToken, refreshTokenExpiresIn, }, }); const authBridgeData = JSON.stringify({ action: myapp.constants.LOGIN, memberId: profile?.memberId, memberNo: profile?.memberNo, memberName: profile?.memberName, nickName: profile?.nickname, solutionAuthenticationKey: ShopbySkin.utils.memberAuth.get(), snsProvider, }); myapp.handler.scheme( `${myapp.helpers.getPackageName()}.auth://${snsProvider}?next=${nextPath}&sessionStorages=${authSchemeData}&bridge=${authBridgeData}` ); return {}; }, }); const modifyMemberProxy = (callback) => new Proxy(callback, { async apply(target, thisArg, argArr) { await target.apply(thisArg, argArr); const { password, profileConfirm } = argArr[0] ?? {}; if (!password.value || !profileConfirm.value) { return; } const isValidPassword = password.isValid && profileConfirm.isValid; const isSamePassword = password.value === profileConfirm.value; const isPasswordModified = isValidPassword && isSamePassword; if (isPasswordModified) { myapp.handler.send({ key: myapp.constants.PASSWORD_MODIFIED, }); } }, }); const modifyPasswordProxy = (callback) => new Proxy(callback, { async apply(target, thisArg, argArr) { await target.apply(thisArg, argArr); myapp.handler.send({ key: myapp.constants.PASSWORD_MODIFIED, }); }, }); const sendBridgeEventWhenAnchorEvent = (e) => { const isAnchor = e.target.tagName === 'A' && e.target instanceof HTMLAnchorElement; if (!isAnchor) { return; } const { href: moveToUrl } = e.target; if (isMoveToSignIn(moveToUrl)) { e?.preventDefault(); const pages = [ShopbySkin.constants.PAGE.SIGN_UP_COMPLETE, ShopbySkin.constants.PAGE.SIGN_UP_METHOD]; const shouldRedirectToMainAfterSignIn = pages.some((page) => location.href.includes(page)); const returnUrl = shouldRedirectToMainAfterSignIn ? `${location.origin}${ShopbySkin.constants.PAGE.MAIN}` : location.href; myapp.handler.send({ key: myapp.constants.LOGINVIEW, option: { returnUrl, showGuestOrder: Boolean(returnUrl.includes(ShopbySkin.constants.PAGE.ORDER_SHEET_FORM)), }, }); } }; class CustomTabMyappInitializer { get proxyFnMap() { return { authCallbackSignInProxy: customTabsAuthCallbackSignInProxy, }; } execute() { this.init(); } init() { this.applyProxy(); } applyProxy() { ShopbySkin.EventManager.on('MYAPP:PROXY_REGISTER', (payload) => { const { proxyFnKey, applyProxy } = payload; const proxyFn = this.proxyFnMap[proxyFnKey]; if (typeof proxyFn === 'function') { applyProxy(proxyFn); } }); } } class MyappInitializer { get proxyFnMap() { return { navigateProxySignIn, sendBridgeEventWhenAnchorEvent, isSignedInProxy, historyReplaceProxy, authCallbackSignInProxy, slideMenuProxy, openIdUrlProxy, withdrawalProxy, signInFormProxy, signUpFormProxy, dormantSignInLayerModalProxy, modifyMemberProxy, modifyPasswordProxy, handleRejectionProxy, }; } execute() { this.init(); } init() { this.applyProxy(); this.addDOMContentLoadedEventHandler(); this.addClickEventHandler(); this.addVisibilityChangeEventHandler(); } addDOMContentLoadedEventHandler() { document.addEventListener('DOMContentLoaded', replaceNextToFrom); } addVisibilityChangeEventHandler() { document.addEventListener('visibilitychange', handleCustomTabsClosed); } addClickEventHandler() { document.addEventListener('click', this.proxyFnMap.sendBridgeEventWhenAnchorEvent); } applyProxy() { ShopbySkin.utils.isSignedIn.applyProxy(this.proxyFnMap.isSignedInProxy); ShopbySkin.utils.moveTo.applyMyappProxy(this.proxyFnMap.navigateProxySignIn); ShopbySkin.utils.replaceHistory.applyMyappProxy(this.proxyFnMap.historyReplaceProxy); ShopbySkin.EventManager.on('MYAPP:PROXY_REGISTER', (payload) => { const { proxyFnKey, applyProxy } = payload; const proxyFn = this.proxyFnMap[proxyFnKey]; if (typeof proxyFn === 'function') { applyProxy(proxyFn); } }); } } if (myapp.helpers.isOpenedAsCustomTab()) { myapp.init({ initializer: new CustomTabMyappInitializer(), }); } else { myapp.init({ initializer: new MyappInitializer(), }); myapp.helpers.hideElement(['bottom-nav nav.nav-bottom', 'button.top-btn']); } })();