Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughReplaces cookie-based push-token persistence with an AsyncStorage-backed in-memory store and event-driven registration; updates WebView integration to message-based token/auth handling; adds push token register/unregister API calls and native access-token storage; introduces a new "stage" build profile and API_ENV-based config branching. Changes
Sequence Diagram(s)sequenceDiagram
participant App as App/_layout
participant Store as PushTokenStore (AsyncStorage)
participant Web as WebView (app/webview/[path].tsx)
participant Auth as NativeAuthStore
participant API as Backend (notifications API)
Note over App,Store: initPushTokenStore() loads token into memory
Store->>App: stored token (if any)
App->>Store: onPushToken(callback) subscription
Web->>App: WebView posts LOGIN_COMPLETE {accessToken}
App->>Auth: saveAccessToken(accessToken)
App->>Store: getStoredToken()
alt stored push token exists
App->>API: registerPushToken(pushToken) using getAccessToken()
API-->>App: 200/OK
App->>Web: postMessage NOTIFICATION_STATUS (success)
else no stored push token
App->>Web: postMessage NOTIFICATION_STATUS (no-token)
end
Web->>App: LOGOUT
App->>Store: getStoredToken()
alt push token exists
App->>API: unregisterPushToken(pushToken)
App->>Auth: clearAccessToken()
else
App->>Auth: clearAccessToken()
end
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly Related PRs
Suggested Reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/_layout.tsx`:
- Around line 49-54: The issue is that shouldRecheckPermission() is being called
on every AppState change which clears the wentToSettings flag too early; move
the call into the active-state branch so the flag is only consumed when
nextAppState === 'active'. Concretely, inside the AppState change handler call
shouldRecheckPermission() only when nextAppState === 'active' and then use its
result to decide whether to call registerForPushNotificationsAsync(); this keeps
wentToSettings in services/notifications.ts intact during non-active transitions
and preserves the intended retry behavior.
| const fromSettings = shouldRecheckPermission(); | ||
| // 설정에서 돌아온 경우: 항상 재시도 | ||
| // 권한 거부가 아닌데 토큰이 없는 경우(네트워크 오류 등): 재시도 | ||
| // 권한 거부 상태에서 그냥 포그라운드 복귀: 재시도 안 함 (alert 무한 반복 방지) | ||
| if (nextAppState === 'active' && (fromSettings || (!tokenObtained && !permissionDenied))) { | ||
| registerForPushNotificationsAsync() |
There was a problem hiding this comment.
Avoid clearing wentToSettings on non-active transitions.
Line 49 calls shouldRecheckPermission() on every AppState change; that resets the flag (see services/notifications.ts) before the app becomes active, so the retry can be skipped after returning from settings. Move the call inside the nextAppState === 'active' branch.
Suggested fix
- const fromSettings = shouldRecheckPermission();
- if (nextAppState === 'active' && (fromSettings || (!tokenObtained && !permissionDenied))) {
- registerForPushNotificationsAsync()
- .then(handleToken)
- .catch((error: any) => console.error(error));
- }
+ if (nextAppState === 'active') {
+ const fromSettings = shouldRecheckPermission();
+ if (fromSettings || (!tokenObtained && !permissionDenied)) {
+ registerForPushNotificationsAsync()
+ .then(handleToken)
+ .catch((error: any) => console.error(error));
+ }
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/_layout.tsx` around lines 49 - 54, The issue is that
shouldRecheckPermission() is being called on every AppState change which clears
the wentToSettings flag too early; move the call into the active-state branch so
the flag is only consumed when nextAppState === 'active'. Concretely, inside the
AppState change handler call shouldRecheckPermission() only when nextAppState
=== 'active' and then use its result to decide whether to call
registerForPushNotificationsAsync(); this keeps wentToSettings in
services/notifications.ts intact during non-active transitions and preserves the
intended retry behavior.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@eas.json`:
- Around line 16-24: The eas.json "stage" profile sets EXPO_PUBLIC_APP_ENV to
"development" while using distribution "store"; update EXPO_PUBLIC_APP_ENV to a
distinct value (e.g., "stage") if this profile should behave differently from
the "development" profile, and then ensure runtime checks that reference
EXPO_PUBLIC_APP_ENV (e.g., code paths that select API base URLs, feature flags,
or logging) explicitly handle the new "stage" value; if the current behavior is
intentional, document that EXPO_PUBLIC_APP_ENV="development" is expected for the
"stage" profile.
| "stage": { | ||
| "distribution": "store", | ||
| "env": { | ||
| "EXPO_PUBLIC_APP_ENV": "development" | ||
| }, | ||
| "ios": { | ||
| "resourceClass": "m-medium" | ||
| } | ||
| }, |
There was a problem hiding this comment.
Confirm EXPO_PUBLIC_APP_ENV: "development" is intentional for a store-distributed build.
distribution: "store" targets TestFlight / App Store channels, but EXPO_PUBLIC_APP_ENV is set to "development" — the same value used by the development profile. If runtime code branches on this variable to select API base URLs, feature flags, or log verbosity, a TestFlight artifact will behave identically to a development build.
If the intent is to have a dedicated staging environment, consider using a distinct value (e.g., "stage") and gating on it appropriately.
🔧 Suggested change if a distinct stage env is intended
"stage": {
"distribution": "store",
"env": {
- "EXPO_PUBLIC_APP_ENV": "development"
+ "EXPO_PUBLIC_APP_ENV": "stage"
},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@eas.json` around lines 16 - 24, The eas.json "stage" profile sets
EXPO_PUBLIC_APP_ENV to "development" while using distribution "store"; update
EXPO_PUBLIC_APP_ENV to a distinct value (e.g., "stage") if this profile should
behave differently from the "development" profile, and then ensure runtime
checks that reference EXPO_PUBLIC_APP_ENV (e.g., code paths that select API base
URLs, feature flags, or logging) explicitly handle the new "stage" value; if the
current behavior is intentional, document that EXPO_PUBLIC_APP_ENV="development"
is expected for the "stage" profile.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
constants/constants.ts (1)
1-6: Normalize/validateAPI_ENVto avoid silent production routing.Any unexpected value (e.g., “stage”) will currently route to production. Consider validating or normalizing to prevent accidental prod traffic.
Example validation
const APP_ENV = process.env.EXPO_PUBLIC_APP_ENV || 'production'; -const API_ENV = process.env.EXPO_PUBLIC_API_ENV || APP_ENV; +const API_ENV = process.env.EXPO_PUBLIC_API_ENV || APP_ENV; +if (API_ENV !== 'development' && API_ENV !== 'production') { + throw new Error(`Invalid EXPO_PUBLIC_API_ENV: ${API_ENV}`); +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@constants/constants.ts` around lines 1 - 6, Normalize and validate API_ENV before using it: convert process.env.EXPO_PUBLIC_API_ENV to lowercase and map known aliases (e.g., 'stage' or 'staging') to 'development'; only accept 'development' or 'production' after normalization, and if the value is unknown default to the non-production safe option (e.g., 'development') so apiUrl and webUrl can't silently route to production. Update the code around APP_ENV, API_ENV, apiUrl and webUrl to perform this normalization/mapping and use the validated value when deciding URLs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app.config.ts`:
- Around line 3-8: APP_ENV/API_ENV mismatch can produce a stage-named app with
production identifiers; update appName, packageName and googleServicesFile to be
derived from API_ENV (or add a fail-fast check) so identifiers align with the
API target. Concretely, in app.config.ts use API_ENV to decide appName,
packageName and googleServicesFile (or add an explicit guard that throws/logs an
error when APP_ENV !== API_ENV) so a build using API_ENV=development cannot
accidentally use production packageName or googleServicesFile; adjust the logic
around APP_ENV, API_ENV, appName, packageName and googleServicesFile
accordingly.
---
Nitpick comments:
In `@constants/constants.ts`:
- Around line 1-6: Normalize and validate API_ENV before using it: convert
process.env.EXPO_PUBLIC_API_ENV to lowercase and map known aliases (e.g.,
'stage' or 'staging') to 'development'; only accept 'development' or
'production' after normalization, and if the value is unknown default to the
non-production safe option (e.g., 'development') so apiUrl and webUrl can't
silently route to production. Update the code around APP_ENV, API_ENV, apiUrl
and webUrl to perform this normalization/mapping and use the validated value
when deciding URLs.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
app.config.tsconstants/constants.tseas.json
🚧 Files skipped from review as they are similar to previous changes (1)
- eas.json
| 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'; |
There was a problem hiding this comment.
Prevent APP_ENV/API_ENV mismatch from producing a stage‑named app with production identifiers.
If API_ENV=development while APP_ENV=production, the app name becomes “KONECT S” but packageName/googleServicesFile stay production, which can overwrite prod installs or bind a stage build to prod Firebase. Consider aligning identifiers with API_ENV or failing fast when envs diverge.
Suggested guard to avoid unsafe env combinations
const APP_ENV = process.env.EXPO_PUBLIC_APP_ENV || 'production';
const API_ENV = process.env.EXPO_PUBLIC_API_ENV || APP_ENV;
+if (APP_ENV === 'production' && API_ENV === 'development') {
+ throw new Error(
+ 'Invalid env combo: API_ENV=development with APP_ENV=production will use prod bundle IDs/Firebase.'
+ );
+}
const appName =
APP_ENV === 'development' ? 'KONECT D' : API_ENV === 'development' ? 'KONECT S' : 'KONECT';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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'; | |
| const APP_ENV = process.env.EXPO_PUBLIC_APP_ENV || 'production'; | |
| const API_ENV = process.env.EXPO_PUBLIC_API_ENV || APP_ENV; | |
| if (APP_ENV === 'production' && API_ENV === 'development') { | |
| throw new Error( | |
| 'Invalid env combo: API_ENV=development with APP_ENV=production will use prod bundle IDs/Firebase.' | |
| ); | |
| } | |
| 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'; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app.config.ts` around lines 3 - 8, APP_ENV/API_ENV mismatch can produce a
stage-named app with production identifiers; update appName, packageName and
googleServicesFile to be derived from API_ENV (or add a fail-fast check) so
identifiers align with the API target. Concretely, in app.config.ts use API_ENV
to decide appName, packageName and googleServicesFile (or add an explicit guard
that throws/logs an error when APP_ENV !== API_ENV) so a build using
API_ENV=development cannot accidentally use production packageName or
googleServicesFile; adjust the logic around APP_ENV, API_ENV, appName,
packageName and googleServicesFile accordingly.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
app.config.ts (1)
3-8:⚠️ Potential issue | 🟠 MajorAlign identifiers with API_ENV to avoid stage/prod mix‑ups.
When
API_ENV=developmentbutAPP_ENV=production, the app name becomes “KONECT S” whilepackageName/googleServicesFileremain production, which can overwrite prod installs or bind stage builds to prod Firebase. Consider deriving identifiers fromAPI_ENVor failing fast on mismatched envs.Suggested guard to avoid unsafe env combinations
const APP_ENV = process.env.EXPO_PUBLIC_APP_ENV || 'production'; const API_ENV = process.env.EXPO_PUBLIC_API_ENV || APP_ENV; +if (APP_ENV === 'production' && API_ENV === 'development') { + throw new Error( + 'Invalid env combo: API_ENV=development with APP_ENV=production will use prod bundle IDs/Firebase.' + ); +} const appName = APP_ENV === 'development' ? 'KONECT D' : API_ENV === 'development' ? 'KONECT S' : 'KONECT';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app.config.ts` around lines 3 - 8, The current logic uses APP_ENV for packageName and googleServicesFile but appName uses API_ENV, which can mix prod/stage artifacts; update the code so all deployment identifiers (appName, packageName, googleServicesFile) are derived from API_ENV (not APP_ENV) or add a fail-fast guard that throws/logs and exits when APP_ENV !== API_ENV; specifically change references to APP_ENV in packageName and googleServicesFile to use API_ENV (and adjust appName to the same source) or add a small validation block that checks APP_ENV === API_ENV and aborts with a clear error to prevent accidental production overwrite.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@app.config.ts`:
- Around line 3-8: The current logic uses APP_ENV for packageName and
googleServicesFile but appName uses API_ENV, which can mix prod/stage artifacts;
update the code so all deployment identifiers (appName, packageName,
googleServicesFile) are derived from API_ENV (not APP_ENV) or add a fail-fast
guard that throws/logs and exits when APP_ENV !== API_ENV; specifically change
references to APP_ENV in packageName and googleServicesFile to use API_ENV (and
adjust appName to the same source) or add a small validation block that checks
APP_ENV === API_ENV and aborts with a clear error to prevent accidental
production overwrite.
| - development | ||
| - preview | ||
| - production | ||
| - stage |
There was a problem hiding this comment.
오 근데 expo 빌드 타입에 stage는 없지 않나요
그래서
development -> stage
production, preview -> production
으로 만들었습니다.
There was a problem hiding this comment.
아 추가하셨군요
근데 그냥 preview를 써도 되지 않나요...?
There was a problem hiding this comment.
development: 개발자용. developmentClient가 활성화되어 Expo Dev Client로 실행, 별도 패키지명/구글서비스로 분리됨
preview: QA/테스트 내부 배포용. 스토어 제출 없이 APK로 내부 팀에게 배포
stage: TestFlight(iOS) 용도로 특수하게 사용. 앱 자체는 production 빌드이나 API는 개발 서버(API_ENV: development)를 바라봄 → 앱 이름이 "KONECT S"로 표시
production: 실제 스토어 배포용. Android는 AAB(Play Store 업로드 형식), iOS는 App Store Connect 제출용
이렇게 생각하고 만들었습니다
preview는 internal distribution 이라 testflight 업로드가 안된다고 해서 일단 임의로 stage를 만들었는데 이번 테스트에서만 사용될 것 같아 불필요하다면 푸시 알림 해결되면 제거해도 괜찮을것 같아요
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
app/_layout.tsx (1)
60-62:⚠️ Potential issue | 🟠 MajorCall
shouldRecheckPermission()only whennextAppState === 'active'.The flag is consumed on every state change right now, so retry-after-settings can be skipped.
Suggested change
- const fromSettings = shouldRecheckPermission(); - if (nextAppState === 'active' && (fromSettings || (!tokenObtained && !permissionDenied))) { - registerForPushNotificationsAsync() - .then(handleToken) - .catch((error: any) => console.error(error)); - } + if (nextAppState === 'active') { + const fromSettings = shouldRecheckPermission(); + if (fromSettings || (!tokenObtained && !permissionDenied)) { + registerForPushNotificationsAsync() + .then(handleToken) + .catch((error: any) => console.error(error)); + } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/_layout.tsx` around lines 60 - 62, The current code calls shouldRecheckPermission() on every app state change; move its invocation inside the active-state check so it only runs when nextAppState === 'active'. Update the condition that uses fromSettings (and variables tokenObtained, permissionDenied) so that fromSettings is computed only when evaluating the active branch and then call registerForPushNotificationsAsync() accordingly; ensure shouldRecheckPermission() is not invoked outside the nextAppState === 'active' branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/webview/`[path].tsx:
- Around line 50-99: handleMessage currently processes any posted message;
restrict it by validating the message origin before acting on auth/token flows.
Add an allowlist (e.g., allowedOrigins or expectedOrigin) and check
event.nativeEvent.origin (or event.nativeEvent.url if origin is not present)
against that allowlist at the top of handleMessage; if the origin is not
allowed, return immediately and do not call JSON.parse or any actions. Keep the
rest of the logic (calls to saveAccessToken, clearAccessToken,
registerPushToken, unregisterPushToken, getStoredToken, and
webViewRef.injectJavaScript) unchanged but only execute them after the origin
check passes.
In `@services/nativeAuthStore.ts`:
- Around line 1-15: Replace AsyncStorage usage with expo-secure-store calls in
this module: update saveAccessToken, getAccessToken, and clearAccessToken to use
SecureStore.setItemAsync, SecureStore.getItemAsync, and
SecureStore.deleteItemAsync respectively (keep the ACCESS_TOKEN_KEY constant),
ensuring async/await and error handling are preserved; also apply the same
replacement for PUSH_TOKEN in utils/pushTokenStore.ts so tokens are stored in
OS-backed encrypted storage instead of AsyncStorage.
In `@services/pushTokenApi.ts`:
- Around line 11-19: registerPushToken and unregisterPushToken call fetch
without timeout and can hang; wrap each fetch call in an AbortController with a
short configurable timeout (e.g., 5–10s): create an AbortController, pass
controller.signal into fetch, start a setTimeout that calls controller.abort()
after the timeout, and clearTimeout on success; catch the abort error (identify
via error.name === 'AbortError') and handle it similarly to other network errors
so the auth lifecycle doesn't stall. Update services/pushTokenApi.ts to use this
pattern for the POST to `${apiUrl}/notifications/tokens` and the corresponding
unregister fetch, keeping Authorization and headers unchanged.
---
Duplicate comments:
In `@app/_layout.tsx`:
- Around line 60-62: The current code calls shouldRecheckPermission() on every
app state change; move its invocation inside the active-state check so it only
runs when nextAppState === 'active'. Update the condition that uses fromSettings
(and variables tokenObtained, permissionDenied) so that fromSettings is computed
only when evaluating the active branch and then call
registerForPushNotificationsAsync() accordingly; ensure
shouldRecheckPermission() is not invoked outside the nextAppState === 'active'
branch.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
app/_layout.tsxapp/webview/[path].tsxservices/nativeAuthStore.tsservices/pushTokenApi.tsutils/pushTokenStore.ts
| const handleMessage = useCallback(async (event: WebViewMessageEvent) => { | ||
| try { | ||
| const data = JSON.parse(event.nativeEvent.data); | ||
| const { type } = data; | ||
|
|
||
| useEffect(() => { | ||
| return onPushToken((token) => { | ||
| if (pageLoadedRef.current) { | ||
| injectPushToken(token); | ||
| } else { | ||
| pendingTokenRef.current = token; | ||
| } | ||
| }); | ||
| }, [injectPushToken]); | ||
| if (type === 'LOGIN_COMPLETE') { | ||
| const { accessToken } = data; | ||
| if (!accessToken) return; | ||
|
|
||
| const handleLoadEnd = useCallback(() => { | ||
| pageLoadedRef.current = true; | ||
| if (pendingTokenRef.current) { | ||
| injectPushToken(pendingTokenRef.current); | ||
| pendingTokenRef.current = null; | ||
| await saveAccessToken(accessToken); | ||
| console.log('LOGIN_COMPLETE: accessToken 저장 완료'); | ||
|
|
||
| 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 파싱 실패 등 무시 | ||
| } | ||
| }, [injectPushToken]); | ||
| }, []); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "[path].tsx" -o -name "*webview*" | grep -i path | head -20Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 53
🏁 Script executed:
fd -t f "path\].tsx"Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 93
🏁 Script executed:
fd -t f "\[path\]\.tsx"Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 93
🏁 Script executed:
git ls-files | grep -i webviewRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 93
🏁 Script executed:
cat -n app/webview/[path].tsx | head -120Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 4852
🏁 Script executed:
rg "webUrl|WebView" app/webview/[path].tsx -A 3 -B 3Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 1514
🏁 Script executed:
rg "onMessage|event.nativeEvent" app/webview/[path].tsx -B 5 -A 5Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 705
🏁 Script executed:
cat -n app/webview/[path].tsx | grep -A 20 "WebView"Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 3216
🏁 Script executed:
cat app/constants/constants.tsRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 129
🏁 Script executed:
web_search
react-native-webview WebViewMessageEvent nativeEvent.url propertyRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 180
🏁 Script executed:
fd -type f "constants" app/Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 241
🏁 Script executed:
rg "webUrl\s*=" --type ts --type tsx -A 2Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 98
🏁 Script executed:
npm list react-native-webview 2>/dev/null || echo "npm unavailable, checking package files"Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 168
🏁 Script executed:
cat package.json | rg "react-native-webview"Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 109
🏁 Script executed:
fd "constants" -e ts -e tsxRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 93
🏁 Script executed:
find . -name "constants*" -type fRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 13406
🏁 Script executed:
rg "export.*webUrl" --type tsRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 188
🏁 Script executed:
cat package.json | head -50Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 1624
🏁 Script executed:
rg "WebViewMessageEvent\|nativeEvent" --no-filename -A 5 -B 5Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 53
🏁 Script executed:
find . -path ./node_modules -prune -o -name "*.ts" -o -name "*.tsx" -exec grep -l "postMessage\|onMessage" {} \;Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 95
🏁 Script executed:
cat ./node_modules/.pnpm/react-native-webview*/node_modules/react-native-webview/lib/WebViewTypes.d.ts 2>/dev/null | head -100Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 4257
🏁 Script executed:
find ./node_modules -path "*react-native-webview*" -name "*.d.ts" -exec cat {} \; | head -200Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 7357
🏁 Script executed:
cat ./node_modules/.pnpm/react-native-webview@13.15.0/node_modules/react-native-webview/lib/WebViewTypes.d.ts | grep -A 20 "WebViewMessage"Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 208
🏁 Script executed:
find ./node_modules -path "*react-native-webview*13.15*" -name "WebViewTypes.d.ts"Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 263
🏁 Script executed:
find ./node_modules -path "*react-native-webview*" -name "WebViewTypes.d.ts" 2>/dev/null | head -1 | xargs catRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 43378
🏁 Script executed:
ls -la ./node_modules/.pnpm/ | grep react-native-webviewRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 239
🏁 Script executed:
cat ./node_modules/.pnpm/react-native-webview@13.15.0_react-native@0.81.5_@babel+core@7.29.0_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/react-native-webview/lib/WebViewTypes.d.ts | grep -A 10 "interface WebViewMessage"Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 338
🏁 Script executed:
cat ./node_modules/.pnpm/react-native-webview@13.15.0_react-native@0.81.5_@babel+core@7.29.0_@types+react@19.1.17_react@19.1.0__react@19.1.0/node_modules/react-native-webview/lib/WebViewTypes.d.ts | grep -B 5 "interface WebViewMessage"Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 250
Validate WebView message origin before handling auth/token actions.
handleMessage currently trusts all posted messages. Any loaded page can send LOGIN_COMPLETE, TOKEN_REFRESH, or LOGOUT payloads and mutate native token state (save/clear access tokens, register/unregister push tokens).
Suggested change
const handleMessage = useCallback(async (event: WebViewMessageEvent) => {
try {
+ const trustedOrigin = new URL(webUrl).origin;
+ const messageUrl = event.nativeEvent.url;
+ if (!messageUrl) return;
+ if (new URL(messageUrl).origin !== trustedOrigin) return;
+
const data = JSON.parse(event.nativeEvent.data);
const { type } = data;
+ if (!['LOGIN_COMPLETE', 'TOKEN_REFRESH', 'LOGOUT'].includes(type)) return;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleMessage = useCallback(async (event: WebViewMessageEvent) => { | |
| try { | |
| const data = JSON.parse(event.nativeEvent.data); | |
| const { type } = data; | |
| useEffect(() => { | |
| return onPushToken((token) => { | |
| if (pageLoadedRef.current) { | |
| injectPushToken(token); | |
| } else { | |
| pendingTokenRef.current = token; | |
| } | |
| }); | |
| }, [injectPushToken]); | |
| if (type === 'LOGIN_COMPLETE') { | |
| const { accessToken } = data; | |
| if (!accessToken) return; | |
| const handleLoadEnd = useCallback(() => { | |
| pageLoadedRef.current = true; | |
| if (pendingTokenRef.current) { | |
| injectPushToken(pendingTokenRef.current); | |
| pendingTokenRef.current = null; | |
| await saveAccessToken(accessToken); | |
| console.log('LOGIN_COMPLETE: accessToken 저장 완료'); | |
| 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 파싱 실패 등 무시 | |
| } | |
| }, [injectPushToken]); | |
| }, []); | |
| const handleMessage = useCallback(async (event: WebViewMessageEvent) => { | |
| try { | |
| const trustedOrigin = new URL(webUrl).origin; | |
| const messageUrl = event.nativeEvent.url; | |
| if (!messageUrl) return; | |
| if (new URL(messageUrl).origin !== trustedOrigin) return; | |
| const data = JSON.parse(event.nativeEvent.data); | |
| const { type } = data; | |
| if (!['LOGIN_COMPLETE', 'TOKEN_REFRESH', 'LOGOUT'].includes(type)) return; | |
| if (type === 'LOGIN_COMPLETE') { | |
| const { accessToken } = data; | |
| if (!accessToken) return; | |
| await saveAccessToken(accessToken); | |
| console.log('LOGIN_COMPLETE: accessToken 저장 완료'); | |
| 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 파싱 실패 등 무시 | |
| } | |
| }, []); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/webview/`[path].tsx around lines 50 - 99, handleMessage currently
processes any posted message; restrict it by validating the message origin
before acting on auth/token flows. Add an allowlist (e.g., allowedOrigins or
expectedOrigin) and check event.nativeEvent.origin (or event.nativeEvent.url if
origin is not present) against that allowlist at the top of handleMessage; if
the origin is not allowed, return immediately and do not call JSON.parse or any
actions. Keep the rest of the logic (calls to saveAccessToken, clearAccessToken,
registerPushToken, unregisterPushToken, getStoredToken, and
webViewRef.injectJavaScript) unchanged but only execute them after the origin
check passes.
services/nativeAuthStore.ts
Outdated
| import AsyncStorage from '@react-native-async-storage/async-storage'; | ||
|
|
||
| const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN'; | ||
|
|
||
| export async function saveAccessToken(token: string): Promise<void> { | ||
| await AsyncStorage.setItem(ACCESS_TOKEN_KEY, token); | ||
| } | ||
|
|
||
| export async function getAccessToken(): Promise<string | null> { | ||
| return AsyncStorage.getItem(ACCESS_TOKEN_KEY); | ||
| } | ||
|
|
||
| export async function clearAccessToken(): Promise<void> { | ||
| await AsyncStorage.removeItem(ACCESS_TOKEN_KEY); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the file and verify it exists
find . -name "nativeAuthStore.ts" -type fRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 100
🏁 Script executed:
# Check project structure and package.json for dependencies
if [ -f "package.json" ]; then
cat package.json | head -50
fiRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 1624
🏁 Script executed:
# Look for any existing secure storage implementations in the codebase
rg -i "securestore|keychain|keystore" --type ts --type js | head -20Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 53
🏁 Script executed:
# Search for other places where ACCESS_TOKEN or similar auth tokens are being stored
rg "AsyncStorage.*setItem|setItem.*token" --type ts --type js | head -20Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 230
🏁 Script executed:
# Check if expo-secure-store is already listed as a dependency
rg "expo-secure-store" package.json package-lock.json yarn.lock 2>/dev/nullRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 350
Replace AsyncStorage with expo-secure-store for storing authentication credentials.
ACCESS_TOKEN is currently stored unencrypted. AsyncStorage is designed for non-sensitive data; bearer tokens must use encrypted, OS-backed secure storage. The project already has expo-secure-store as a dependency, so this is a straightforward substitution.
Suggested change
-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<void> {
- await AsyncStorage.setItem(ACCESS_TOKEN_KEY, token);
+ await SecureStore.setItemAsync(ACCESS_TOKEN_KEY, token);
}
export async function getAccessToken(): Promise<string | null> {
- return AsyncStorage.getItem(ACCESS_TOKEN_KEY);
+ return SecureStore.getItemAsync(ACCESS_TOKEN_KEY);
}
export async function clearAccessToken(): Promise<void> {
- await AsyncStorage.removeItem(ACCESS_TOKEN_KEY);
+ await SecureStore.deleteItemAsync(ACCESS_TOKEN_KEY);
}Note: utils/pushTokenStore.ts has the same issue with PUSH_TOKEN.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import AsyncStorage from '@react-native-async-storage/async-storage'; | |
| const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN'; | |
| export async function saveAccessToken(token: string): Promise<void> { | |
| await AsyncStorage.setItem(ACCESS_TOKEN_KEY, token); | |
| } | |
| export async function getAccessToken(): Promise<string | null> { | |
| return AsyncStorage.getItem(ACCESS_TOKEN_KEY); | |
| } | |
| export async function clearAccessToken(): Promise<void> { | |
| await AsyncStorage.removeItem(ACCESS_TOKEN_KEY); | |
| } | |
| import * as SecureStore from 'expo-secure-store'; | |
| const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN'; | |
| export async function saveAccessToken(token: string): Promise<void> { | |
| await SecureStore.setItemAsync(ACCESS_TOKEN_KEY, token); | |
| } | |
| export async function getAccessToken(): Promise<string | null> { | |
| return SecureStore.getItemAsync(ACCESS_TOKEN_KEY); | |
| } | |
| export async function clearAccessToken(): Promise<void> { | |
| await SecureStore.deleteItemAsync(ACCESS_TOKEN_KEY); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/nativeAuthStore.ts` around lines 1 - 15, Replace AsyncStorage usage
with expo-secure-store calls in this module: update saveAccessToken,
getAccessToken, and clearAccessToken to use SecureStore.setItemAsync,
SecureStore.getItemAsync, and SecureStore.deleteItemAsync respectively (keep the
ACCESS_TOKEN_KEY constant), ensuring async/await and error handling are
preserved; also apply the same replacement for PUSH_TOKEN in
utils/pushTokenStore.ts so tokens are stored in OS-backed encrypted storage
instead of AsyncStorage.
services/pushTokenApi.ts
Outdated
| const res = await fetch(`${apiUrl}/notifications/tokens`, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| body: JSON.stringify({ token: pushToken }), | ||
| }); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd pushTokenApi.tsRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 95
🏁 Script executed:
cat services/pushTokenApi.tsRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 1295
🏁 Script executed:
rg "AbortController|setTimeout.*abort|timeout" --type ts --type tsx --type js --type jsx -iRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 98
🏁 Script executed:
rg "fetch" --type ts --type tsx --type js --type jsx -B 2 -A 5 | head -100Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 98
🏁 Script executed:
rg "AbortController|clearTimeout|REQUEST_TIMEOUT" -t ts -t jsRepository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 53
🏁 Script executed:
rg "fetch" -t ts -t js -A 3 | head -80Repository: BCSDLab/KONECT_REACT_NATIVE
Length of output: 767
Add timeout/abort handling for token API calls.
Both registerPushToken and unregisterPushToken use bare fetch() without timeout, which can hang indefinitely on poor networks during critical auth lifecycle operations. Add an abort-based timeout to prevent stalls.
Suggested implementation
+const REQUEST_TIMEOUT_MS = 10000;
+
+async function fetchWithTimeout(input: RequestInfo, init: RequestInit, timeoutMs = REQUEST_TIMEOUT_MS) {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
+ try {
+ return await fetch(input, { ...init, signal: controller.signal });
+ } finally {
+ clearTimeout(timeoutId);
+ }
+}
+
export async function registerPushToken(pushToken: string): Promise<void> {
@@
- const res = await fetch(`${apiUrl}/notifications/tokens`, {
+ const res = await fetchWithTimeout(`${apiUrl}/notifications/tokens`, {
@@
export async function unregisterPushToken(pushToken: string): Promise<void> {
@@
- const res = await fetch(`${apiUrl}/notifications/tokens`, {
+ const res = await fetchWithTimeout(`${apiUrl}/notifications/tokens`, {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const res = await fetch(`${apiUrl}/notifications/tokens`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| Authorization: `Bearer ${accessToken}`, | |
| }, | |
| body: JSON.stringify({ token: pushToken }), | |
| }); | |
| const res = await fetchWithTimeout(`${apiUrl}/notifications/tokens`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| Authorization: `Bearer ${accessToken}`, | |
| }, | |
| body: JSON.stringify({ token: pushToken }), | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/pushTokenApi.ts` around lines 11 - 19, registerPushToken and
unregisterPushToken call fetch without timeout and can hang; wrap each fetch
call in an AbortController with a short configurable timeout (e.g., 5–10s):
create an AbortController, pass controller.signal into fetch, start a setTimeout
that calls controller.abort() after the timeout, and clearTimeout on success;
catch the abort error (identify via error.name === 'AbortError') and handle it
similarly to other network errors so the auth lifecycle doesn't stall. Update
services/pushTokenApi.ts to use this pattern for the POST to
`${apiUrl}/notifications/tokens` and the corresponding unregister fetch, keeping
Authorization and headers unchanged.
Summary by CodeRabbit
Bug Fixes
New Features
Chores