From a9dec1c58a964170f124169c9b5a13b3aa50f348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 25 Feb 2026 00:15:15 +0900 Subject: [PATCH 1/8] =?UTF-8?q?fix:=20js=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/_layout.tsx | 25 +++++++++++++----------- app/webview/[path].tsx | 43 +++++++++++++++++++---------------------- utils/pushTokenStore.ts | 2 ++ 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/app/_layout.tsx b/app/_layout.tsx index 281c9eb..2ce5050 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -4,7 +4,6 @@ import { AppState } from 'react-native'; import { getForceUpdate, appVersion, versionToNumber } from '../services/forceupdate'; import * as Notifications from 'expo-notifications'; import { registerForPushNotificationsAsync, shouldRecheckPermission } from '../services/notifications'; -import CookieManager from '@react-native-cookies/cookies'; import { storePushToken } from '../utils/pushTokenStore'; Notifications.setNotificationHandler({ @@ -16,14 +15,6 @@ Notifications.setNotificationHandler({ }), }); -function addTokenToCookie(token: string) { - CookieManager.set('https://agit.gg', { - name: 'EXPO_PUSH_TOKEN', - value: token, - domain: '.agit.gg', - path: '/', - }); -} export default function RootLayout() { const { replace } = useRouter(); @@ -34,11 +25,19 @@ export default function RootLayout() { }, []); useEffect(() => { + let tokenObtained = false; + // 권한 거부로 토큰 취득 불가한 경우 (네트워크 오류와 구분) + // registerForPushNotificationsAsync가 undefined를 반환하면 권한 거부 + let permissionDenied = false; + const handleToken = (token?: string) => { if (token) { - addTokenToCookie(token); + tokenObtained = true; + permissionDenied = false; storePushToken(token); console.log('Expo Push Token:', token); + } else { + permissionDenied = true; } }; @@ -47,7 +46,11 @@ export default function RootLayout() { .catch((error: any) => console.error(error)); const subscription = AppState.addEventListener('change', (nextAppState) => { - if (nextAppState === 'active' && shouldRecheckPermission()) { + const fromSettings = shouldRecheckPermission(); + // 설정에서 돌아온 경우: 항상 재시도 + // 권한 거부가 아닌데 토큰이 없는 경우(네트워크 오류 등): 재시도 + // 권한 거부 상태에서 그냥 포그라운드 복귀: 재시도 안 함 (alert 무한 반복 방지) + if (nextAppState === 'active' && (fromSettings || (!tokenObtained && !permissionDenied))) { registerForPushNotificationsAsync() .then(handleToken) .catch((error: any) => console.error(error)); diff --git a/app/webview/[path].tsx b/app/webview/[path].tsx index d47b783..2ac46f6 100644 --- a/app/webview/[path].tsx +++ b/app/webview/[path].tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect, useCallback } from 'react'; +import { useRef, useEffect, useCallback, useState } from 'react'; import { BackHandler, Platform, @@ -16,9 +16,14 @@ import CookieManager from '@react-native-cookies/cookies'; import { generateUserAgent } from '../../utils/userAgent'; import { ShouldStartLoadRequest } from 'react-native-webview/lib/WebViewTypes'; import { webUrl } from '../../constants/constants'; -import { onPushToken } from '../../utils/pushTokenStore'; +import { onPushToken, getStoredToken } from '../../utils/pushTokenStore'; const ALLOWED_URL_SCHEMES = ['kakaotalk', 'nidlogin']; + +function buildPushTokenScript(token: string): string { + const safeToken = JSON.stringify(token); + return `(function(){window.dispatchEvent(new MessageEvent('message',{data:JSON.stringify({type:'PUSH_TOKEN',token:${safeToken}})}));}());true;`; +} const userAgent = generateUserAgent(); const handleOnShouldStartLoadWithRequest = ({ url }: ShouldStartLoadRequest) => { @@ -43,36 +48,27 @@ export default function Index() { const webViewRef = useRef(null); const canGoBackRef = useRef(false); const pageLoadedRef = useRef(false); - const pendingTokenRef = useRef(null); + const [pushToken, setPushToken] = useState(getStoredToken); const local = useLocalSearchParams(); + useEffect(() => { + return onPushToken(setPushToken); + }, []); + const injectPushToken = useCallback((token: string) => { - const safeToken = JSON.stringify(token); - webViewRef.current?.injectJavaScript(` - window.dispatchEvent(new MessageEvent('message', { - data: JSON.stringify({ type: 'PUSH_TOKEN', token: ${safeToken} }) - })); - true; - `); + webViewRef.current?.injectJavaScript(buildPushTokenScript(token)); }, []); + // 페이지 로드 후 토큰이 도착한 경우 직접 주입 useEffect(() => { - return onPushToken((token) => { - if (pageLoadedRef.current) { - injectPushToken(token); - } else { - pendingTokenRef.current = token; - } - }); - }, [injectPushToken]); + if (pushToken && pageLoadedRef.current) { + injectPushToken(pushToken); + } + }, [pushToken, injectPushToken]); const handleLoadEnd = useCallback(() => { pageLoadedRef.current = true; - if (pendingTokenRef.current) { - injectPushToken(pendingTokenRef.current); - pendingTokenRef.current = null; - } - }, [injectPushToken]); + }, []); useEffect(() => { if (Platform.OS === 'android') { @@ -122,6 +118,7 @@ export default function Index() { originWhitelist={['*']} startInLoadingState onLoadEnd={handleLoadEnd} + injectedJavaScript={pushToken ? buildPushTokenScript(pushToken) : undefined} /> ); diff --git a/utils/pushTokenStore.ts b/utils/pushTokenStore.ts index 160af38..6d79247 100644 --- a/utils/pushTokenStore.ts +++ b/utils/pushTokenStore.ts @@ -7,6 +7,8 @@ export const storePushToken = (token: string) => { _callbacks.length = 0; }; +export const getStoredToken = (): string | null => _token; + export const onPushToken = (cb: (token: string) => void): (() => void) => { if (_token) { cb(_token); From 77cc8be93b162cff82edb388d4691204ccdbd30c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 25 Feb 2026 00:25:14 +0900 Subject: [PATCH 2/8] =?UTF-8?q?chore:=20=EC=8A=A4=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20+=20testflight=20=EC=9A=A9=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ios_build.yml | 1 + eas.json | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/ios_build.yml b/.github/workflows/ios_build.yml index 6b83c9c..64b69cc 100644 --- a/.github/workflows/ios_build.yml +++ b/.github/workflows/ios_build.yml @@ -11,6 +11,7 @@ on: - development - preview - production + - stage jobs: build: runs-on: macos-26 diff --git a/eas.json b/eas.json index 347dbef..19dde3e 100644 --- a/eas.json +++ b/eas.json @@ -13,6 +13,15 @@ "resourceClass": "m-medium" } }, + "stage": { + "distribution": "store", + "env": { + "EXPO_PUBLIC_APP_ENV": "development" + }, + "ios": { + "resourceClass": "m-medium" + } + }, "preview": { "distribution": "internal", "android": { From c226c9d3cc3f42207195e1e21244102105c8d07d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 25 Feb 2026 00:32:38 +0900 Subject: [PATCH 3/8] =?UTF-8?q?chore:=20=EC=8A=A4=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20+=20testflight=20=EC=9A=A9=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.config.ts | 3 ++- constants/constants.ts | 5 +++-- eas.json | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app.config.ts b/app.config.ts index a9d1520..6001250 100644 --- a/app.config.ts +++ b/app.config.ts @@ -1,7 +1,8 @@ import { ExpoConfig } from 'expo/config'; const APP_ENV = process.env.EXPO_PUBLIC_APP_ENV || 'production'; -const appName = APP_ENV === 'development' ? 'KONECT D' : 'KONECT'; +const API_ENV = process.env.EXPO_PUBLIC_API_ENV || APP_ENV; +const appName = APP_ENV === 'development' ? 'KONECT D' : API_ENV === 'development' ? 'KONECT S' : 'KONECT'; const packageName = APP_ENV === 'development' ? 'com.bcsdlab.konect.dev' : 'com.bcsdlab.konect'; const googleServicesFile = APP_ENV === 'development' ? './google-services-debug.json' : './google-services.json'; diff --git a/constants/constants.ts b/constants/constants.ts index 2beee39..b08029f 100644 --- a/constants/constants.ts +++ b/constants/constants.ts @@ -1,5 +1,6 @@ const APP_ENV = process.env.EXPO_PUBLIC_APP_ENV || 'production'; +const API_ENV = process.env.EXPO_PUBLIC_API_ENV || APP_ENV; export const apiUrl = - APP_ENV === 'development' ? 'https://api.stage.agit.gg' : 'https://api.agit.gg'; -export const webUrl = APP_ENV === 'development' ? 'https://stage.agit.gg' : 'https://agit.gg'; + API_ENV === 'development' ? 'https://api.stage.agit.gg' : 'https://api.agit.gg'; +export const webUrl = API_ENV === 'development' ? 'https://stage.agit.gg' : 'https://agit.gg'; diff --git a/eas.json b/eas.json index 19dde3e..f483e58 100644 --- a/eas.json +++ b/eas.json @@ -16,7 +16,8 @@ "stage": { "distribution": "store", "env": { - "EXPO_PUBLIC_APP_ENV": "development" + "EXPO_PUBLIC_APP_ENV": "production", + "EXPO_PUBLIC_API_ENV": "development" }, "ios": { "resourceClass": "m-medium" From a9ac42062c09dc666c59adf25745d79631961211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 25 Feb 2026 01:15:03 +0900 Subject: [PATCH 4/8] =?UTF-8?q?chore:=20=EB=B9=8C=EB=93=9C=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.config.ts b/app.config.ts index 6001250..9614c19 100644 --- a/app.config.ts +++ b/app.config.ts @@ -21,7 +21,7 @@ const config: ExpoConfig = { supportsTablet: true, usesAppleSignIn: true, bundleIdentifier: packageName, - buildNumber: '1010500', + buildNumber: '1010602', infoPlist: { ITSAppUsesNonExemptEncryption: false, }, From 9b0c338eb55a322a90bf299a1324fc5c9d29337d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 25 Feb 2026 17:21:34 +0900 Subject: [PATCH 5/8] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=84=A4=EC=9D=B4=ED=8B=B0=EB=B8=8C=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B8=8C=EB=A6=BF=EC=A7=80=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/_layout.tsx | 24 ++++++++---- app/webview/[path].tsx | 77 +++++++++++++++++++++++++------------ services/nativeAuthStore.ts | 15 ++++++++ services/pushTokenApi.ts | 44 +++++++++++++++++++++ utils/pushTokenStore.ts | 14 ++++++- 5 files changed, 140 insertions(+), 34 deletions(-) create mode 100644 services/nativeAuthStore.ts create mode 100644 services/pushTokenApi.ts diff --git a/app/_layout.tsx b/app/_layout.tsx index 2ce5050..107f545 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -4,7 +4,9 @@ import { AppState } from 'react-native'; import { getForceUpdate, appVersion, versionToNumber } from '../services/forceupdate'; import * as Notifications from 'expo-notifications'; import { registerForPushNotificationsAsync, shouldRecheckPermission } from '../services/notifications'; -import { storePushToken } from '../utils/pushTokenStore'; +import { storePushToken, initPushTokenStore, getStoredToken } from '../utils/pushTokenStore'; +import { getAccessToken } from '../services/nativeAuthStore'; +import { registerPushToken } from '../services/pushTokenApi'; Notifications.setNotificationHandler({ handleNotification: async () => ({ @@ -24,18 +26,27 @@ export default function RootLayout() { setIsReady(true); }, []); + useEffect(() => { + initPushTokenStore(); + }, []); + useEffect(() => { let tokenObtained = false; - // 권한 거부로 토큰 취득 불가한 경우 (네트워크 오류와 구분) - // registerForPushNotificationsAsync가 undefined를 반환하면 권한 거부 let permissionDenied = false; - const handleToken = (token?: string) => { + const handleToken = async (token?: string) => { if (token) { tokenObtained = true; permissionDenied = false; - storePushToken(token); + await storePushToken(token); console.log('Expo Push Token:', token); + + const accessToken = await getAccessToken(); + if (accessToken) { + registerPushToken(token).catch((e) => + console.error('자동 푸시 토큰 등록 실패:', e) + ); + } } else { permissionDenied = true; } @@ -47,9 +58,6 @@ export default function RootLayout() { const subscription = AppState.addEventListener('change', (nextAppState) => { const fromSettings = shouldRecheckPermission(); - // 설정에서 돌아온 경우: 항상 재시도 - // 권한 거부가 아닌데 토큰이 없는 경우(네트워크 오류 등): 재시도 - // 권한 거부 상태에서 그냥 포그라운드 복귀: 재시도 안 함 (alert 무한 반복 방지) if (nextAppState === 'active' && (fromSettings || (!tokenObtained && !permissionDenied))) { registerForPushNotificationsAsync() .then(handleToken) diff --git a/app/webview/[path].tsx b/app/webview/[path].tsx index 2ac46f6..ea07287 100644 --- a/app/webview/[path].tsx +++ b/app/webview/[path].tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect, useCallback, useState } from 'react'; +import { useRef, useEffect, useCallback } from 'react'; import { BackHandler, Platform, @@ -10,20 +10,18 @@ import { AppStateStatus, } from 'react-native'; import { Slot, useLocalSearchParams } from 'expo-router'; -import { WebView } from 'react-native-webview'; +import { WebView, WebViewMessageEvent } from 'react-native-webview'; import { SafeAreaView } from 'react-native-safe-area-context'; import CookieManager from '@react-native-cookies/cookies'; import { generateUserAgent } from '../../utils/userAgent'; import { ShouldStartLoadRequest } from 'react-native-webview/lib/WebViewTypes'; import { webUrl } from '../../constants/constants'; -import { onPushToken, getStoredToken } from '../../utils/pushTokenStore'; +import { getStoredToken } from '../../utils/pushTokenStore'; +import { saveAccessToken, clearAccessToken } from '../../services/nativeAuthStore'; +import { registerPushToken, unregisterPushToken } from '../../services/pushTokenApi'; const ALLOWED_URL_SCHEMES = ['kakaotalk', 'nidlogin']; -function buildPushTokenScript(token: string): string { - const safeToken = JSON.stringify(token); - return `(function(){window.dispatchEvent(new MessageEvent('message',{data:JSON.stringify({type:'PUSH_TOKEN',token:${safeToken}})}));}());true;`; -} const userAgent = generateUserAgent(); const handleOnShouldStartLoadWithRequest = ({ url }: ShouldStartLoadRequest) => { @@ -47,27 +45,57 @@ const handleOnShouldStartLoadWithRequest = ({ url }: ShouldStartLoadRequest) => export default function Index() { const webViewRef = useRef(null); const canGoBackRef = useRef(false); - const pageLoadedRef = useRef(false); - const [pushToken, setPushToken] = useState(getStoredToken); const local = useLocalSearchParams(); - useEffect(() => { - return onPushToken(setPushToken); - }, []); + const handleMessage = useCallback(async (event: WebViewMessageEvent) => { + try { + const data = JSON.parse(event.nativeEvent.data); + const { type } = data; - const injectPushToken = useCallback((token: string) => { - webViewRef.current?.injectJavaScript(buildPushTokenScript(token)); - }, []); + if (type === 'LOGIN_COMPLETE') { + const { accessToken } = data; + if (!accessToken) return; - // 페이지 로드 후 토큰이 도착한 경우 직접 주입 - useEffect(() => { - if (pushToken && pageLoadedRef.current) { - injectPushToken(pushToken); - } - }, [pushToken, injectPushToken]); + await saveAccessToken(accessToken); + console.log('LOGIN_COMPLETE: accessToken 저장 완료'); - const handleLoadEnd = useCallback(() => { - pageLoadedRef.current = true; + const pushToken = getStoredToken(); + if (pushToken) { + try { + await registerPushToken(pushToken); + console.log('푸시 토큰 백엔드 등록 완료'); + webViewRef.current?.injectJavaScript( + `window.dispatchEvent(new CustomEvent('NOTIFICATION_STATUS', { detail: { registered: true } }));true;` + ); + } catch (e) { + console.error('푸시 토큰 등록 실패:', e); + webViewRef.current?.injectJavaScript( + `window.dispatchEvent(new CustomEvent('NOTIFICATION_STATUS', { detail: { registered: false } }));true;` + ); + } + } + } else if (type === 'TOKEN_REFRESH') { + const { accessToken } = data; + if (accessToken) { + await saveAccessToken(accessToken); + console.log('TOKEN_REFRESH: accessToken 갱신 완료'); + } + } else if (type === 'LOGOUT') { + const pushToken = getStoredToken(); + if (pushToken) { + try { + await unregisterPushToken(pushToken); + console.log('푸시 토큰 백엔드 삭제 완료'); + } catch (e) { + console.error('푸시 토큰 삭제 실패:', e); + } + } + await clearAccessToken(); + console.log('LOGOUT: accessToken 삭제 완료'); + } + } catch { + // JSON 파싱 실패 등 무시 + } }, []); useEffect(() => { @@ -117,8 +145,7 @@ export default function Index() { onShouldStartLoadWithRequest={handleOnShouldStartLoadWithRequest} originWhitelist={['*']} startInLoadingState - onLoadEnd={handleLoadEnd} - injectedJavaScript={pushToken ? buildPushTokenScript(pushToken) : undefined} + onMessage={handleMessage} /> ); diff --git a/services/nativeAuthStore.ts b/services/nativeAuthStore.ts new file mode 100644 index 0000000..5c63eb9 --- /dev/null +++ b/services/nativeAuthStore.ts @@ -0,0 +1,15 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; + +const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN'; + +export async function saveAccessToken(token: string): Promise { + await AsyncStorage.setItem(ACCESS_TOKEN_KEY, token); +} + +export async function getAccessToken(): Promise { + return AsyncStorage.getItem(ACCESS_TOKEN_KEY); +} + +export async function clearAccessToken(): Promise { + await AsyncStorage.removeItem(ACCESS_TOKEN_KEY); +} diff --git a/services/pushTokenApi.ts b/services/pushTokenApi.ts new file mode 100644 index 0000000..dfa2092 --- /dev/null +++ b/services/pushTokenApi.ts @@ -0,0 +1,44 @@ +import { apiUrl } from '../constants/constants'; +import { getAccessToken } from './nativeAuthStore'; + +export async function registerPushToken(pushToken: string): Promise { + const accessToken = await getAccessToken(); + if (!accessToken) { + console.warn('registerPushToken: accessToken 없음, 등록 생략'); + return; + } + + const res = await fetch(`${apiUrl}/notifications/tokens`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ token: pushToken }), + }); + + if (!res.ok) { + throw new Error(`registerPushToken failed: ${res.status}`); + } +} + +export async function unregisterPushToken(pushToken: string): Promise { + const accessToken = await getAccessToken(); + if (!accessToken) { + console.warn('unregisterPushToken: accessToken 없음, 삭제 생략'); + return; + } + + const res = await fetch(`${apiUrl}/notifications/tokens`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ token: pushToken }), + }); + + if (!res.ok) { + throw new Error(`unregisterPushToken failed: ${res.status}`); + } +} diff --git a/utils/pushTokenStore.ts b/utils/pushTokenStore.ts index 6d79247..f6c8203 100644 --- a/utils/pushTokenStore.ts +++ b/utils/pushTokenStore.ts @@ -1,8 +1,20 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; + +const PUSH_TOKEN_KEY = 'PUSH_TOKEN'; + let _token: string | null = null; const _callbacks: ((token: string) => void)[] = []; -export const storePushToken = (token: string) => { +export async function initPushTokenStore(): Promise { + const saved = await AsyncStorage.getItem(PUSH_TOKEN_KEY); + if (saved) { + _token = saved; + } +} + +export const storePushToken = async (token: string) => { _token = token; + await AsyncStorage.setItem(PUSH_TOKEN_KEY, token); _callbacks.forEach((cb) => cb(token)); _callbacks.length = 0; }; From d5c0aaae0fe995fde9efde63f7b5dddecfc6a20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 25 Feb 2026 17:30:47 +0900 Subject: [PATCH 6/8] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/_layout.tsx | 4 +++- app/webview/[path].tsx | 6 ++++++ services/nativeAuthStore.ts | 8 ++++---- services/pushTokenApi.ts | 15 +++++++++++++-- utils/pushTokenStore.ts | 6 +++--- 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/app/_layout.tsx b/app/_layout.tsx index 107f545..0d1aacf 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -57,8 +57,10 @@ export default function RootLayout() { .catch((error: any) => console.error(error)); const subscription = AppState.addEventListener('change', (nextAppState) => { + if (nextAppState !== 'active') return; + const fromSettings = shouldRecheckPermission(); - if (nextAppState === 'active' && (fromSettings || (!tokenObtained && !permissionDenied))) { + if (fromSettings || (!tokenObtained && !permissionDenied)) { registerForPushNotificationsAsync() .then(handleToken) .catch((error: any) => console.error(error)); diff --git a/app/webview/[path].tsx b/app/webview/[path].tsx index ea07287..a26eaa7 100644 --- a/app/webview/[path].tsx +++ b/app/webview/[path].tsx @@ -21,6 +21,7 @@ import { saveAccessToken, clearAccessToken } from '../../services/nativeAuthStor import { registerPushToken, unregisterPushToken } from '../../services/pushTokenApi'; const ALLOWED_URL_SCHEMES = ['kakaotalk', 'nidlogin']; +const ALLOWED_ORIGINS = [new URL(webUrl).origin]; const userAgent = generateUserAgent(); @@ -48,6 +49,11 @@ export default function Index() { const local = useLocalSearchParams(); const handleMessage = useCallback(async (event: WebViewMessageEvent) => { + const origin = event.nativeEvent.url; + if (!origin || !ALLOWED_ORIGINS.some((allowed) => origin.startsWith(allowed))) { + return; + } + try { const data = JSON.parse(event.nativeEvent.data); const { type } = data; diff --git a/services/nativeAuthStore.ts b/services/nativeAuthStore.ts index 5c63eb9..75399aa 100644 --- a/services/nativeAuthStore.ts +++ b/services/nativeAuthStore.ts @@ -1,15 +1,15 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; +import * as SecureStore from 'expo-secure-store'; const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN'; export async function saveAccessToken(token: string): Promise { - await AsyncStorage.setItem(ACCESS_TOKEN_KEY, token); + await SecureStore.setItemAsync(ACCESS_TOKEN_KEY, token); } export async function getAccessToken(): Promise { - return AsyncStorage.getItem(ACCESS_TOKEN_KEY); + return SecureStore.getItemAsync(ACCESS_TOKEN_KEY); } export async function clearAccessToken(): Promise { - await AsyncStorage.removeItem(ACCESS_TOKEN_KEY); + await SecureStore.deleteItemAsync(ACCESS_TOKEN_KEY); } diff --git a/services/pushTokenApi.ts b/services/pushTokenApi.ts index dfa2092..7815ccd 100644 --- a/services/pushTokenApi.ts +++ b/services/pushTokenApi.ts @@ -1,6 +1,17 @@ import { apiUrl } from '../constants/constants'; import { getAccessToken } from './nativeAuthStore'; +const REQUEST_TIMEOUT_MS = 10_000; + +function fetchWithTimeout(url: string, options: RequestInit): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + + return fetch(url, { ...options, signal: controller.signal }).finally(() => + clearTimeout(timeoutId) + ); +} + export async function registerPushToken(pushToken: string): Promise { const accessToken = await getAccessToken(); if (!accessToken) { @@ -8,7 +19,7 @@ export async function registerPushToken(pushToken: string): Promise { return; } - const res = await fetch(`${apiUrl}/notifications/tokens`, { + const res = await fetchWithTimeout(`${apiUrl}/notifications/tokens`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -29,7 +40,7 @@ export async function unregisterPushToken(pushToken: string): Promise { return; } - const res = await fetch(`${apiUrl}/notifications/tokens`, { + const res = await fetchWithTimeout(`${apiUrl}/notifications/tokens`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', diff --git a/utils/pushTokenStore.ts b/utils/pushTokenStore.ts index f6c8203..d92b177 100644 --- a/utils/pushTokenStore.ts +++ b/utils/pushTokenStore.ts @@ -1,4 +1,4 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; +import * as SecureStore from 'expo-secure-store'; const PUSH_TOKEN_KEY = 'PUSH_TOKEN'; @@ -6,7 +6,7 @@ let _token: string | null = null; const _callbacks: ((token: string) => void)[] = []; export async function initPushTokenStore(): Promise { - const saved = await AsyncStorage.getItem(PUSH_TOKEN_KEY); + const saved = await SecureStore.getItemAsync(PUSH_TOKEN_KEY); if (saved) { _token = saved; } @@ -14,7 +14,7 @@ export async function initPushTokenStore(): Promise { export const storePushToken = async (token: string) => { _token = token; - await AsyncStorage.setItem(PUSH_TOKEN_KEY, token); + await SecureStore.setItemAsync(PUSH_TOKEN_KEY, token); _callbacks.forEach((cb) => cb(token)); _callbacks.length = 0; }; From 69343322feb43efa29021a5c847d0245264f8d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 25 Feb 2026 17:31:15 +0900 Subject: [PATCH 7/8] =?UTF-8?q?chore:=20=EB=B9=8C=EB=93=9C=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.config.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app.config.ts b/app.config.ts index 9614c19..dcb875a 100644 --- a/app.config.ts +++ b/app.config.ts @@ -2,7 +2,8 @@ import { ExpoConfig } from 'expo/config'; const APP_ENV = process.env.EXPO_PUBLIC_APP_ENV || 'production'; const API_ENV = process.env.EXPO_PUBLIC_API_ENV || APP_ENV; -const appName = APP_ENV === 'development' ? 'KONECT D' : API_ENV === 'development' ? 'KONECT S' : 'KONECT'; +const appName = + APP_ENV === 'development' ? 'KONECT D' : API_ENV === 'development' ? 'KONECT S' : 'KONECT'; const packageName = APP_ENV === 'development' ? 'com.bcsdlab.konect.dev' : 'com.bcsdlab.konect'; const googleServicesFile = APP_ENV === 'development' ? './google-services-debug.json' : './google-services.json'; @@ -21,7 +22,7 @@ const config: ExpoConfig = { supportsTablet: true, usesAppleSignIn: true, bundleIdentifier: packageName, - buildNumber: '1010602', + buildNumber: '1010603', infoPlist: { ITSAppUsesNonExemptEncryption: false, }, From 03df4a3ace7395077b672c26adf35559e7960f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 25 Feb 2026 18:48:27 +0900 Subject: [PATCH 8/8] =?UTF-8?q?chore:=20ios=20=EB=B0=B0=ED=8F=AC=EB=A5=BC?= =?UTF-8?q?=20=EC=9C=84=ED=95=9C=20=EB=B9=8C=EB=93=9C=20=EB=84=98=EB=B2=84?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.config.ts b/app.config.ts index dcb875a..136cd5b 100644 --- a/app.config.ts +++ b/app.config.ts @@ -22,7 +22,7 @@ const config: ExpoConfig = { supportsTablet: true, usesAppleSignIn: true, bundleIdentifier: packageName, - buildNumber: '1010603', + buildNumber: '1010604', infoPlist: { ITSAppUsesNonExemptEncryption: false, },