Skip to content

[fix] 쿠키 방식에서 js 이벤트 방식으로 변경#47

Merged
ff1451 merged 8 commits intomainfrom
46-fix-알림-토큰-로직-변경
Feb 25, 2026

Hidden character warning

The head ref may contain hidden characters: "46-fix-\uc54c\ub9bc-\ud1a0\ud070-\ub85c\uc9c1-\ubcc0\uacbd"
Merged

[fix] 쿠키 방식에서 js 이벤트 방식으로 변경#47
ff1451 merged 8 commits intomainfrom
46-fix-알림-토큰-로직-변경

Conversation

@ff1451
Copy link
Contributor

@ff1451 ff1451 commented Feb 24, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Improved push notification reliability and permission-state handling; re-attempts registration when returning from settings or after transient failures.
  • New Features

    • Webview now receives and handles notification/auth events more reliably (login, token refresh, logout) for timely push registration/unregistration.
    • App name now reflects environment variants (distinct names for development/stage/production).
  • Chores

    • Added a new "stage" build profile for staged releases.

@ff1451 ff1451 self-assigned this Feb 24, 2026
@ff1451 ff1451 linked an issue Feb 24, 2026 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces 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

Cohort / File(s) Summary
App Layout
app/_layout.tsx
Remove CookieManager token persistence; initialize initPushTokenStore; use in-memory flags (tokenObtained, permissionDenied); store tokens via storePushToken; attempt registerPushToken when access token exists; adjust AppState permission re-check logic.
WebView Integration
app/webview/[path].tsx
Replace pending/ref-based token flow with onMessage handler processing LOGIN_COMPLETE, TOKEN_REFRESH, LOGOUT; use getStoredToken() and saveAccessToken(); register/unregister push tokens via API; remove pageLoadedRef/pendingTokenRef and onLoadEnd injection.
Push Token Store
utils/pushTokenStore.ts
Add initPushTokenStore() to preload token from AsyncStorage; make storePushToken async and persist to AsyncStorage; add getStoredToken() accessor; update onPushToken to invoke immediately if token present.
Native Auth Store
services/nativeAuthStore.ts
Add AsyncStorage-backed saveAccessToken, getAccessToken, and clearAccessToken for persisting access tokens.
Push Token API
services/pushTokenApi.ts
Add registerPushToken(pushToken) and unregisterPushToken(pushToken) which POST/DELETE to ${apiUrl}/notifications/tokens using Bearer getAccessToken().
Config & Constants
app.config.ts, constants/constants.ts
Introduce EXPO_PUBLIC_API_ENV / internal API_ENV; branch appName, apiUrl, webUrl logic on API_ENV; bump iOS buildNumber.
Build Profiles / CI
.github/workflows/ios_build.yml, eas.json
Add new stage build profile to eas.json (distribution: store, env updates, ios.resourceClass) and allow stage in workflow_dispatch inputs.
Minor
other touched files
Small wiring to integrate new modules and remove cookie/token helpers from prior flow.

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
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

  • [fix] 브릿지 로직 추가 #45: Modifies the same push-token bridge logic and WebView/token integration across app/_layout.tsx, app/webview/[path].tsx, and utils/pushTokenStore.ts.
  • release: v1.0.4 #37: Updates build/version config (touches app.config.ts and buildNumber), related to release/profile changes.

Suggested Reviewers

  • kongwoojin

Poem

🐰
I hopped through code to find the key,
From cookies freed, now stored in memory,
I whisper tokens into WebView ears,
Register, unregister — no more tears,
A carrot-push of tiny cheers 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main architectural change: transitioning from cookie-based to JavaScript event-driven token handling across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 46-fix-알림-토큰-로직-변경

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0d01804 and a9dec1c.

📒 Files selected for processing (3)
  • app/_layout.tsx
  • app/webview/[path].tsx
  • utils/pushTokenStore.ts

Comment on lines +49 to 54
const fromSettings = shouldRecheckPermission();
// 설정에서 돌아온 경우: 항상 재시도
// 권한 거부가 아닌데 토큰이 없는 경우(네트워크 오류 등): 재시도
// 권한 거부 상태에서 그냥 포그라운드 복귀: 재시도 안 함 (alert 무한 반복 방지)
if (nextAppState === 'active' && (fromSettings || (!tokenObtained && !permissionDenied))) {
registerForPushNotificationsAsync()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9dec1c and 77cc8be.

📒 Files selected for processing (2)
  • .github/workflows/ios_build.yml
  • eas.json

Comment on lines +16 to +24
"stage": {
"distribution": "store",
"env": {
"EXPO_PUBLIC_APP_ENV": "development"
},
"ios": {
"resourceClass": "m-medium"
}
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
constants/constants.ts (1)

1-6: Normalize/validate API_ENV to 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

📥 Commits

Reviewing files that changed from the base of the PR and between 77cc8be and c226c9d.

📒 Files selected for processing (3)
  • app.config.ts
  • constants/constants.ts
  • eas.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • eas.json

Comment on lines 3 to 8
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';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
app.config.ts (1)

3-8: ⚠️ Potential issue | 🟠 Major

Align identifiers with API_ENV to avoid stage/prod mix‑ups.

When API_ENV=development but APP_ENV=production, the app name becomes “KONECT S” while packageName/googleServicesFile remain production, which can overwrite prod installs or bind stage builds to prod Firebase. Consider deriving identifiers from API_ENV or 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.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c226c9d and a9ac420.

📒 Files selected for processing (1)
  • app.config.ts

- development
- preview
- production
- stage
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 근데 expo 빌드 타입에 stage는 없지 않나요
그래서

development -> stage
production, preview -> production

으로 만들었습니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 추가하셨군요
근데 그냥 preview를 써도 되지 않나요...?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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를 만들었는데 이번 테스트에서만 사용될 것 같아 불필요하다면 푸시 알림 해결되면 제거해도 괜찮을것 같아요

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
app/_layout.tsx (1)

60-62: ⚠️ Potential issue | 🟠 Major

Call shouldRecheckPermission() only when nextAppState === '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

📥 Commits

Reviewing files that changed from the base of the PR and between a9ac420 and 9b0c338.

📒 Files selected for processing (5)
  • app/_layout.tsx
  • app/webview/[path].tsx
  • services/nativeAuthStore.ts
  • services/pushTokenApi.ts
  • utils/pushTokenStore.ts

Comment on lines +50 to +99
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]);
}, []);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "[path].tsx" -o -name "*webview*" | grep -i path | head -20

Repository: 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 webview

Repository: BCSDLab/KONECT_REACT_NATIVE

Length of output: 93


🏁 Script executed:

cat -n app/webview/[path].tsx | head -120

Repository: BCSDLab/KONECT_REACT_NATIVE

Length of output: 4852


🏁 Script executed:

rg "webUrl|WebView" app/webview/[path].tsx -A 3 -B 3

Repository: BCSDLab/KONECT_REACT_NATIVE

Length of output: 1514


🏁 Script executed:

rg "onMessage|event.nativeEvent" app/webview/[path].tsx -B 5 -A 5

Repository: 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.ts

Repository: BCSDLab/KONECT_REACT_NATIVE

Length of output: 129


🏁 Script executed:

web_search
react-native-webview WebViewMessageEvent nativeEvent.url property

Repository: 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 2

Repository: 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 tsx

Repository: BCSDLab/KONECT_REACT_NATIVE

Length of output: 93


🏁 Script executed:

find . -name "constants*" -type f

Repository: BCSDLab/KONECT_REACT_NATIVE

Length of output: 13406


🏁 Script executed:

rg "export.*webUrl" --type ts

Repository: BCSDLab/KONECT_REACT_NATIVE

Length of output: 188


🏁 Script executed:

cat package.json | head -50

Repository: BCSDLab/KONECT_REACT_NATIVE

Length of output: 1624


🏁 Script executed:

rg "WebViewMessageEvent\|nativeEvent" --no-filename -A 5 -B 5

Repository: 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 -100

Repository: BCSDLab/KONECT_REACT_NATIVE

Length of output: 4257


🏁 Script executed:

find ./node_modules -path "*react-native-webview*" -name "*.d.ts" -exec cat {} \; | head -200

Repository: 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 cat

Repository: BCSDLab/KONECT_REACT_NATIVE

Length of output: 43378


🏁 Script executed:

ls -la ./node_modules/.pnpm/ | grep react-native-webview

Repository: 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.

Suggested change
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.

Comment on lines +1 to +15
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);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the file and verify it exists
find . -name "nativeAuthStore.ts" -type f

Repository: 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
fi

Repository: 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 -20

Repository: 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 -20

Repository: 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/null

Repository: 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.

Suggested change
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.

Comment on lines +11 to +19
const res = await fetch(`${apiUrl}/notifications/tokens`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ token: pushToken }),
});

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd pushTokenApi.ts

Repository: BCSDLab/KONECT_REACT_NATIVE

Length of output: 95


🏁 Script executed:

cat services/pushTokenApi.ts

Repository: BCSDLab/KONECT_REACT_NATIVE

Length of output: 1295


🏁 Script executed:

rg "AbortController|setTimeout.*abort|timeout" --type ts --type tsx --type js --type jsx -i

Repository: 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 -100

Repository: BCSDLab/KONECT_REACT_NATIVE

Length of output: 98


🏁 Script executed:

rg "AbortController|clearTimeout|REQUEST_TIMEOUT" -t ts -t js

Repository: BCSDLab/KONECT_REACT_NATIVE

Length of output: 53


🏁 Script executed:

rg "fetch" -t ts -t js -A 3 | head -80

Repository: 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.

Suggested change
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.

Copy link
Member

@kongwoojin kongwoojin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

멋있어요

@ff1451 ff1451 merged commit c2ebdad into main Feb 25, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[fix] 알림 토큰 로직 변경

2 participants