Repository: OneKeyHQ/app-monorepo
Pull request #7465: feat: provide callback for app unlock event in UI layer
Author: login: huhuanming
Created: 2025-05-30T08:49:47Z | Merged: 2025-05-30T09:12:41Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added support for awaiting app unlock events, allowing certain actions (such as update reminders) to be triggered only after the app is unlocked.
  - Introduced a mechanism to queue and process unlock jobs, improving the handling of unlock-related tasks.
- **Improvements**
  - Update dialogs are now shown after the app is unlocked, enhancing user experience during app updates.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/packages/kit-bg/src/services/ServiceApp.ts b/packages/kit-bg/src/services/ServiceApp.ts
index a833342d8af1..a49658e7535a 100644
--- a/packages/kit-bg/src/services/ServiceApp.ts
+++ b/packages/kit-bg/src/services/ServiceApp.ts
@@ -9,6 +9,10 @@ import {
   isAvailable,
   logoutFromGoogleDrive,
 } from '@onekeyhq/shared/src/cloudfs';
+import {
+  EAppEventBusNames,
+  appEventBus,
+} from '@onekeyhq/shared/src/eventBus/appEventBus';
 import { defaultLogger } from '@onekeyhq/shared/src/logger/logger';
 import { exitApp } from '@onekeyhq/shared/src/modules3rdParty/react-native-exit';
 import platformEnv from '@onekeyhq/shared/src/platformEnv';
@@ -37,6 +41,8 @@ import type { ISimpleDBAppStatus } from '../dbs/simple/entity/SimpleDbEntityAppS
 
 @backgroundClass()
 class ServiceApp extends ServiceBase {
+  unlockJobIds: string[] = [];
+
   constructor({ backgroundApi }: { backgroundApi: any }) {
     super({ backgroundApi });
   }
@@ -270,6 +276,21 @@ class ServiceApp extends ServiceBase {
     return appIsLocked.get();
   }
 
+  @backgroundMethod()
+  async addUnlockJob(jobId: string) {
+    this.unlockJobIds.push(jobId);
+  }
+
+  @backgroundMethod()
+  async dispatchUnlockJob() {
+    while (this.unlockJobIds.length > 0) {
+      const jobId = this.unlockJobIds.pop();
+      if (jobId) {
+        appEventBus.emit(EAppEventBusNames.UnlockApp, { jobId });
+      }
+    }
+  }
+
   @backgroundMethod()
   async openExtensionExpandTab(routeInfo: IOpenUrlRouteInfo) {
     await extUtils.openExpandTab(routeInfo);
diff --git a/packages/kit-bg/src/services/ServicePassword/index.ts b/packages/kit-bg/src/services/ServicePassword/index.ts
index d2b306192492..58a1b32f692f 100644
--- a/packages/kit-bg/src/services/ServicePassword/index.ts
+++ b/packages/kit-bg/src/services/ServicePassword/index.ts
@@ -827,6 +827,7 @@ export default class ServicePassword extends ServiceBase {
       unLock: true,
       manualLocking: false,
     }));
+    await this.backgroundApi.serviceApp.dispatchUnlockJob();
   }
 
   @backgroundMethod()
diff --git a/packages/kit/src/components/UpdateReminder/hooks.tsx b/packages/kit/src/components/UpdateReminder/hooks.tsx
index 5224b34ab0fe..ea4071d3e137 100644
--- a/packages/kit/src/components/UpdateReminder/hooks.tsx
+++ b/packages/kit/src/components/UpdateReminder/hooks.tsx
@@ -34,6 +34,7 @@ import timerUtils from '@onekeyhq/shared/src/utils/timerUtils';
 import backgroundApiProxy from '../../background/instance/backgroundApiProxy';
 import useAppNavigation from '../../hooks/useAppNavigation';
 import { usePromiseResult } from '../../hooks/usePromiseResult';
+import { whenAppUnlocked } from '../../utils/passwordUtils';
 
 const MIN_EXECUTION_DURATION = 3000; // 3 seconds minimum execution time
 
@@ -361,7 +362,7 @@ export const useAppUpdateInfo = (isFullModal = false, autoCheck = true) => {
       void verifyPackage();
     } else {
       void checkForUpdates().then(
-        ({ isNeedUpdate: needUpdate, isForceUpdate, response }) => {
+        async ({ isNeedUpdate: needUpdate, isForceUpdate, response }) => {
           if (needUpdate) {
             if (isForceUpdate) {
               toUpdatePreviewPage(true, response);
@@ -371,7 +372,10 @@ export const useAppUpdateInfo = (isFullModal = false, autoCheck = true) => {
               response?.isShowUpdateDialog &&
               isFirstLaunch
             ) {
-              showUpdateDialog(false, response);
+              await whenAppUnlocked();
+              setTimeout(() => {
+                showUpdateDialog(false, response);
+              }, 200);
             }
           }
           isFirstLaunch = false;
diff --git a/packages/kit/src/utils/passwordUtils.ts b/packages/kit/src/utils/passwordUtils.ts
index 82ea7580f618..9e637b3ec79b 100644
--- a/packages/kit/src/utils/passwordUtils.ts
+++ b/packages/kit/src/utils/passwordUtils.ts
@@ -1,3 +1,11 @@
+import uuid from 'react-native-uuid';
+
+import type { IAppEventBusPayload } from '@onekeyhq/shared/src/eventBus/appEventBus';
+import {
+  EAppEventBusNames,
+  appEventBus,
+} from '@onekeyhq/shared/src/eventBus/appEventBus';
+
 import backgroundApiProxy from '../background/instance/backgroundApiProxy';
 
 export const withPromptPasswordVerify = async <T>({
@@ -18,3 +26,27 @@ export const withPromptPasswordVerify = async <T>({
     await backgroundApiProxy.servicePassword.closePasswordSecuritySession();
   }
 };
+
+export const whenAppUnlocked = () => {
+  return new Promise<void>((resolve) => {
+    void backgroundApiProxy.serviceApp.isAppLocked().then(async (isLock) => {
+      if (!isLock) {
+        resolve();
+        return;
+      }
+      const unlockJobId = uuid.v1().toString();
+      const callback = (
+        event: IAppEventBusPayload[EAppEventBusNames.UnlockApp],
+      ) => {
+        if (event.jobId === unlockJobId) {
+          setTimeout(() => {
+            resolve();
+          }, 100);
+          appEventBus.off(EAppEventBusNames.UnlockApp, callback);
+        }
+      };
+      appEventBus.on(EAppEventBusNames.UnlockApp, callback);
+      await backgroundApiProxy.serviceApp.addUnlockJob(unlockJobId);
+    });
+  });
+};
diff --git a/packages/shared/src/eventBus/appEventBus.ts b/packages/shared/src/eventBus/appEventBus.ts
index 5630c0d0a410..a01448471010 100644
--- a/packages/shared/src/eventBus/appEventBus.ts
+++ b/packages/shared/src/eventBus/appEventBus.ts
@@ -274,6 +274,9 @@ export interface IAppEventBusPayload {
   [EAppEventBusNames.HardwareFeaturesUpdate]: {
     deviceId: string;
   };
+  [EAppEventBusNames.UnlockApp]: {
+    jobId: string;
+  };
 }
 
 export enum EEventBusBroadcastMethodNames {
diff --git a/packages/shared/src/eventBus/appEventBusNames.ts b/packages/shared/src/eventBus/appEventBusNames.ts
index 63ba23161e37..7aa7ce902c5b 100644
--- a/packages/shared/src/eventBus/appEventBusNames.ts
+++ b/packages/shared/src/eventBus/appEventBusNames.ts
@@ -85,6 +85,7 @@ export enum EAppEventBusNames {
   CheckWalletBackupStatus = 'CheckWalletBackupStatus',
   HomeTabsChanged = 'HomeTabsChanged',
   HardwareFeaturesUpdate = 'HardwareFeaturesUpdate',
+  UnlockApp = 'UnlockApp',
   // AccountNameChanged = 'AccountNameChanged',
   // CurrencyChanged = 'CurrencyChanged',
   // BackupRequired = 'BackupRequired',


Reviews:
user: login: ezailWang
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T08:53:39Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 6**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1c3369076453f7fa308740f73520103b0be9af54 and 8c05fef83f59e458c13322fd34c067356d5336cf.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `packages/kit-bg/src/services/ServiceApp.ts` (3 hunks)
* `packages/kit-bg/src/services/ServicePassword/index.ts` (1 hunks)
* `packages/kit/src/components/UpdateReminder/hooks.tsx` (3 hunks)
* `packages/kit/src/utils/passwordUtils.ts` (2 hunks)
* `packages/shared/src/eventBus/appEventBus.ts` (1 hunks)
* `packages/shared/src/eventBus/appEventBusNames.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>packages/kit-bg/src/services/ServiceApp.ts (2)</summary><blockquote>

<details>
<summary>packages/shared/src/background/backgroundDecorators.ts (1)</summary>

* `backgroundMethod` (218-218)

</details>
<details>
<summary>packages/shared/src/eventBus/appEventBus.ts (2)</summary>

* `appEventBus` (457-457)
* `EAppEventBusNames` (457-457)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: unittest (20.x)
* GitHub Check: lint (20.x)
* GitHub Check: Analyze (javascript-typescript)

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>packages/shared/src/eventBus/appEventBusNames.ts (1)</summary>

`88-88`: **Clean addition!**

The new `UnlockApp` event name follows the existing naming pattern and clearly describes its purpose.

</details>
<details>
<summary>packages/shared/src/eventBus/appEventBus.ts (1)</summary>

`277-279`: **Well-structured payload type!**

The `UnlockApp` event payload follows the established pattern and clearly defines the `jobId` string property.

</details>
<details>
<summary>packages/kit/src/components/UpdateReminder/hooks.tsx (1)</summary>

`365-365`: **Good async conversion!**

Converting to async function enables proper await usage for the unlock mechanism.

</details>
<details>
<summary>packages/kit/src/utils/passwordUtils.ts (1)</summary>

`1-8`: **Clean imports setup.**

The new imports for UUID generation and event bus integration are well-structured and properly typed.

</details>
<details>
<summary>packages/kit-bg/src/services/ServiceApp.ts (3)</summary>

`12-15`: **Event bus imports properly added.**

Clean integration with the existing event bus infrastructure.

---

`44-44`: **Job queue property correctly defined.**

Simple and effective job ID storage.

---

`284-292`: <details>
<summary>❓ Verification inconclusive</summary>

**Consider FIFO processing order.**

The method uses `pop()` which processes jobs in LIFO (last-in-first-out) order. For job queues, FIFO is typically more intuitive.


```diff
@backgroundMethod()
async dispatchUnlockJob() {
  while (this.unlockJobIds.length > 0) {
-   const jobId = this.unlockJobIds.pop();
+   const jobId = this.unlockJobIds.shift();
    if (jobId) {
      appEventBus.emit(EAppEventBusNames.UnlockApp, { jobId });
    }
  }
}
```

**Current LIFO behavior may be intentional** - verify if processing order matters for your use case.

---

🏁 Script executed:

```shell
#!/bin/bash
set -e

# Locate ServiceApp.ts
file=$(fd ServiceApp.ts | head -n 1)
echo "Inspecting file: $file"

# Show all occurrences of unlockJobIds
echo "Occurrences of 'unlockJobIds':"
rg -n "unlockJobIds" "$file"

# Show context around its declaration and uses
echo -e "\nContext around declaration and queue operations:"
rg -C3 "unlockJobIds" -n "$file"
```

Length of output: 1301

---



</details>

**Consider using FIFO order for dispatchUnlockJob**

The method currently uses `pop()`, which processes jobs in LIFO order. For a typical job queue, FIFO (using `shift()`) ensures tasks run in the order they were added.

• File: packages/kit-bg/src/services/ServiceApp.ts  
• Declaration: `unlockJobIds: string[] = []` (line 44)  
• Job add: `this.unlockJobIds.push(jobId)` (line 281)  
• Dispatch: `this.unlockJobIds.pop()` (line 287)  

Suggested change:
```diff
   @backgroundMethod()
   async dispatchUnlockJob() {
     while (this.unlockJobIds.length > 0) {
-      const jobId = this.unlockJobIds.pop();
+      const jobId = this.unlockJobIds.shift();
       if (jobId) {
         appEventBus.emit(EAppEventBusNames.UnlockApp, { jobId });
       }
     }
   }
```

Please confirm whether LIFO was intentional or if FIFO better fits your use case.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T08:57:57Z
user: login: weatherstar
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T09:02:42Z
user: login: weatherstar
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T09:05:26Z
user: login: originalix
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-30T09:12:40Z

Review comments:
diff_hunk: @@ -827,6 +827,7 @@ export default class ServicePassword extends ServiceBase {
       unLock: true,
       manualLocking: false,
     }));
+    await this.backgroundApi.serviceApp.dispatchUnlockJob();
path: packages/kit-bg/src/services/ServicePassword/index.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify the method exists and consider error handling.**

The placement after unlock state changes makes sense. Ensure the `dispatchUnlockJob()` method exists in `ServiceApp`.



---

🏁 Script executed:

```shell
#!/bin/bash
# Verify dispatchUnlockJob method exists in ServiceApp
ast-grep --pattern $'class ServiceApp {
  $$$
  dispatchUnlockJob($$$) {
    $$$
  }
  $$$
}'
```

Length of output: 94

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for dispatchUnlockJob definition across the repo
rg -n "dispatchUnlockJob" .

# Locate the ServiceApp class to confirm available methods
rg -n "class ServiceApp" .
```

Length of output: 561

---



</details>

**Wrap dispatchUnlockJob call in error handling**

We’ve confirmed that `dispatchUnlockJob()` is defined in `packages/kit-bg/src/services/ServiceApp.ts:285`. To guard against runtime failures, wrap the await call in a try/catch block:

• Location: `packages/kit-bg/src/services/ServicePassword/index.ts` around line 830  
• Suggested diff:
```diff
-   await this.backgroundApi.serviceApp.dispatchUnlockJob();
+   try {
+     await this.backgroundApi.serviceApp.dispatchUnlockJob();
+   } catch (error) {
+     console.error('Failed to dispatch unlock job', error);
+     // TODO: handle or propagate error as needed
+   }
```

This ensures any errors during job dispatch are caught and handled. 

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion
    try {
      await this.backgroundApi.serviceApp.dispatchUnlockJob();
    } catch (error) {
      console.error('Failed to dispatch unlock job', error);
      // TODO: handle or propagate error as needed
    }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/kit-bg/src/services/ServicePassword/index.ts at line 830, the call
to await this.backgroundApi.serviceApp.dispatchUnlockJob() should be wrapped in
a try/catch block to handle potential runtime errors. Modify the code to enclose
the await call within try { ... } catch (error) { ... } and handle or log the
error appropriately to prevent unhandled promise rejections.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-30T08:57:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -371,7 +372,10 @@ export const useAppUpdateInfo = (isFullModal = false, autoCheck = true) => {
               response?.isShowUpdateDialog &&
               isFirstLaunch
             ) {
-              showUpdateDialog(false, response);
+              await whenAppUnlocked();
+              setTimeout(() => {
+                showUpdateDialog(false, response);
+              }, 200);
path: packages/kit/src/components/UpdateReminder/hooks.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling and clarify the delay.**

The unlock await logic is good, but consider these improvements:

1. Add error handling around `whenAppUnlocked()`
2. Clarify why 200ms delay is needed



```diff
-              await whenAppUnlocked();
-              setTimeout(() => {
-                showUpdateDialog(false, response);
-              }, 200);
+              try {
+                await whenAppUnlocked();
+                // Brief delay to ensure UI is ready after unlock
+                setTimeout(() => {
+                  showUpdateDialog(false, response);
+                }, 200);
+              } catch (error) {
+                console.warn('Failed to wait for app unlock:', error);
+                // Show dialog anyway if unlock waiting fails
+                showUpdateDialog(false, response);
+              }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion
              try {
                await whenAppUnlocked();
                // Brief delay to ensure UI is ready after unlock
                setTimeout(() => {
                  showUpdateDialog(false, response);
                }, 200);
              } catch (error) {
                console.warn('Failed to wait for app unlock:', error);
                // Show dialog anyway if unlock waiting fails
                showUpdateDialog(false, response);
              }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/kit/src/components/UpdateReminder/hooks.tsx around lines 375 to 378,
add a try-catch block around the await call to whenAppUnlocked() to handle any
potential errors gracefully. Inside the catch, log or handle the error
appropriately. Also, add a comment explaining the reason for the 200ms delay
before calling showUpdateDialog to clarify its purpose for future maintainers.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-30T08:57:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,6 +34,7 @@ import timerUtils from '@onekeyhq/shared/src/utils/timerUtils';
 import backgroundApiProxy from '../../background/instance/backgroundApiProxy';
 import useAppNavigation from '../../hooks/useAppNavigation';
 import { usePromiseResult } from '../../hooks/usePromiseResult';
+import { whenAppUnlocked } from '../../utils/passwordUtils';
path: packages/kit/src/components/UpdateReminder/hooks.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify the import exists.**

Ensure the `whenAppUnlocked` function is properly exported from the password utils module.

---

🏁 Script executed:

```shell
#!/bin/bash
# Verify whenAppUnlocked function exists in passwordUtils
ast-grep --pattern $'export $$$function whenAppUnlocked($$$) {
  $$$
}'
```

Length of output: 72

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for all occurrences of whenAppUnlocked, including its definition and export
rg -n "whenAppUnlocked"
```

Length of output: 318

---



</details>

**Export Verified: `whenAppUnlocked` Exists**

I’ve confirmed that `whenAppUnlocked` is exported from `packages/kit/src/utils/passwordUtils.ts`. The integration in `hooks.tsx` looks solid. A couple of follow-ups before merging:

• Add error handling around the unlock call  
  - Wrap `await whenAppUnlocked()` in try/catch.  
  - Decide on a fallback (e.g., log the error or proceed immediately).

• Clarify the 200 ms delay  
  - Why 200 ms?  
  - Consider extracting it into a named constant or adding a comment.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/kit/src/components/UpdateReminder/hooks.tsx at line 37, wrap the
call to await whenAppUnlocked() in a try/catch block to handle potential errors
gracefully, such as logging the error or defining a fallback behavior.
Additionally, for the 200 ms delay used after unlocking, extract this value into
a clearly named constant or add a comment explaining the reason for this
specific delay to improve code clarity and maintainability.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-30T08:57:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,3 +26,26 @@ export const withPromptPasswordVerify = async <T>({
     await backgroundApiProxy.servicePassword.closePasswordSecuritySession();
   }
 };
+
+export const whenAppUnlocked = () => {
+  return new Promise<void>((resolve) => {
+    void backgroundApiProxy.serviceApp.isAppLocked().then(async (isLock) => {
+      if (!isLock) {
+        resolve();
+      }
+      const unlockJobId = uuid.v1().toString();
+      const callback = (
+        event: IAppEventBusPayload[EAppEventBusNames.UnlockApp],
+      ) => {
+        if (event.jobId === unlockJobId) {
+          setTimeout(() => {
+            resolve();
+          }, 100);
path: packages/kit/src/utils/passwordUtils.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Document the 100ms delay purpose.**

Why the 100ms delay? Add a comment explaining the timing requirement.


```diff
setTimeout(() => {
  resolve();
+  // Allow time for unlock state to propagate
}, 100);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion
          setTimeout(() => {
            resolve();
            // Allow time for unlock state to propagate
          }, 100);
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/kit/src/utils/passwordUtils.ts around lines 41 to 43, the setTimeout
uses a 100ms delay without explanation. Add a comment above the setTimeout
explaining why the 100ms delay is necessary, clarifying the timing requirement
or purpose for this wait to improve code readability and maintainability.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-30T08:57:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,3 +26,26 @@ export const withPromptPasswordVerify = async <T>({
     await backgroundApiProxy.servicePassword.closePasswordSecuritySession();
   }
 };
+
+export const whenAppUnlocked = () => {
+  return new Promise<void>((resolve) => {
+    void backgroundApiProxy.serviceApp.isAppLocked().then(async (isLock) => {
+      if (!isLock) {
+        resolve();
+      }
+      const unlockJobId = uuid.v1().toString();
+      const callback = (
+        event: IAppEventBusPayload[EAppEventBusNames.UnlockApp],
+      ) => {
+        if (event.jobId === unlockJobId) {
+          setTimeout(() => {
+            resolve();
+          }, 100);
+          appEventBus.off(EAppEventBusNames.UnlockApp, callback);
+        }
+      };
+      appEventBus.on(EAppEventBusNames.UnlockApp, callback);
+      await backgroundApiProxy.serviceApp.addUnlockJob(unlockJobId);
+    });
+  });
+};
path: packages/kit/src/utils/passwordUtils.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

_⚠️ Potential issue_

**Fix race condition and missing return statement.**

Two critical issues need attention:

1. **Missing return after immediate resolve** - When the app isn't locked, the function continues executing instead of returning early.
2. **Race condition** - If the app unlocks between the lock check and event listener setup, the promise never resolves.



Apply this fix:

```diff
export const whenAppUnlocked = () => {
  return new Promise<void>((resolve) => {
    void backgroundApiProxy.serviceApp.isAppLocked().then(async (isLock) => {
      if (!isLock) {
        resolve();
+       return;
      }
      const unlockJobId = uuid.v1().toString();
+     
+     // Register job first to avoid race condition
+     await backgroundApiProxy.serviceApp.addUnlockJob(unlockJobId);
+     
      const callback = (
        event: IAppEventBusPayload[EAppEventBusNames.UnlockApp],
      ) => {
        if (event.jobId === unlockJobId) {
          setTimeout(() => {
            resolve();
          }, 100);
          appEventBus.off(EAppEventBusNames.UnlockApp, callback);
        }
      };
      appEventBus.on(EAppEventBusNames.UnlockApp, callback);
-     await backgroundApiProxy.serviceApp.addUnlockJob(unlockJobId);
    });
  });
};
```

**Consider adding error handling and timeout** for robustness.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion
export const whenAppUnlocked = () => {
  return new Promise<void>((resolve) => {
    void backgroundApiProxy.serviceApp.isAppLocked().then(async (isLock) => {
      if (!isLock) {
        resolve();
        return;
      }
      const unlockJobId = uuid.v1().toString();

      // Register unlock job before listener to avoid race conditions
      await backgroundApiProxy.serviceApp.addUnlockJob(unlockJobId);

      const callback = (
        event: IAppEventBusPayload[EAppEventBusNames.UnlockApp],
      ) => {
        if (event.jobId === unlockJobId) {
          setTimeout(() => {
            resolve();
          }, 100);
          appEventBus.off(EAppEventBusNames.UnlockApp, callback);
        }
      };
      appEventBus.on(EAppEventBusNames.UnlockApp, callback);
    });
  });
};
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/kit/src/utils/passwordUtils.ts lines 30 to 51, fix the missing
return after resolving immediately when the app is not locked by adding a return
statement to stop further execution. To address the race condition where the app
might unlock between the lock check and event listener setup, check the lock
state again after adding the event listener and resolve if unlocked.
Additionally, enhance robustness by adding error handling and a timeout
mechanism to ensure the promise resolves or rejects appropriately.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-30T08:57:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -270,6 +276,21 @@ class ServiceApp extends ServiceBase {
     return appIsLocked.get();
   }
 
+  @backgroundMethod()
+  async addUnlockJob(jobId: string) {
+    this.unlockJobIds.push(jobId);
+  }
path: packages/kit-bg/src/services/ServiceApp.ts
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Method works correctly but could be synchronous.**

The implementation is solid. Consider removing `async` since you're just pushing to an array.


```diff
@backgroundMethod()
-async addUnlockJob(jobId: string) {
+addUnlockJob(jobId: string) {
  this.unlockJobIds.push(jobId);
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion
  @backgroundMethod()
  addUnlockJob(jobId: string) {
    this.unlockJobIds.push(jobId);
  }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In packages/kit-bg/src/services/ServiceApp.ts around lines 279 to 282, the
addUnlockJob method is marked async but only performs a synchronous operation of
pushing a jobId to an array. Remove the async keyword from the method
declaration to make it synchronous, as there is no asynchronous code inside the
method.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-30T08:57:56Z
author_association: CONTRIBUTOR

PR comments:
user: login: codesandbox[bot]
created_at: 2025-05-30T08:49:50Z
body: #### Review or Edit in CodeSandbox

Open the branch in <a href="https://codesandbox.io/p/github/OneKeyHQ/app-monorepo/when-ready?mode=review&utm_source=gh_app">Web Editor</a> &bull; <a href="https://codesandbox.io/p/vscode?owner=OneKeyHQ&repo=app-monorepo&branch=when-ready&utm_source=gh_app">VS Code</a> &bull; <a href="https://codesandbox.io/p/vscode?owner=OneKeyHQ&repo=app-monorepo&branch=when-ready&insiders=true&utm_source=gh_app">Insiders</a><br>
Open <a href="https://codesandbox.io/p/devtool/preview/OneKeyHQ/app-monorepo/when-ready?task=preview&port=51423&redirect=true&utm_source=gh_app">Preview</a>

<!-- open-in-codesandbox:complete -->

author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-30T08:49:54Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update adds a job queue system for unlock events in the app. It introduces new methods to manage unlock jobs, updates the unlock workflow to dispatch these jobs, and provides a utility to await app unlock. Event bus types and names are extended to support the new unlock event.

## Changes

| Files/Paths                             | Change Summary                                                                                      |
|---------------------------------------|--------------------------------------------------------------------------------------------------|
| `.../ServiceApp.ts`                    | Added `unlockJobIds` array, `addUnlockJob`, and `dispatchUnlockJob` methods to manage unlock jobs and emit unlock events. |
| `.../ServicePassword/index.ts`         | Updated `unLockApp` to call `dispatchUnlockJob` after unlocking.                                  |
| `.../UpdateReminder/hooks.tsx`         | Deferred update dialog display until after app unlock using new `whenAppUnlocked` utility.        |
| `.../passwordUtils.ts`                  | Added `whenAppUnlocked` function to await app unlock via event bus and job ID mechanism.          |
| `.../appEventBus.ts`                    | Added `UnlockApp` event payload with `jobId` to event bus interface.                              |
| `.../appEventBusNames.ts`               | Added `UnlockApp` to event bus names enum.                                                        |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Component
    participant passwordUtils
    participant ServiceApp
    participant AppEventBus

    Component->>passwordUtils: whenAppUnlocked()
    passwordUtils->>ServiceApp: isAppLocked()
    alt App is unlocked
        passwordUtils-->>Component: Resolve immediately
    else App is locked
        passwordUtils->>ServiceApp: addUnlockJob(jobId)
        ServiceApp->>ServiceApp: Store jobId in unlockJobIds
        passwordUtils->>AppEventBus: Subscribe to UnlockApp event with jobId
        ServiceApp->>ServiceApp: Later, dispatchUnlockJob()
        loop For each jobId
            ServiceApp->>AppEventBus: Emit UnlockApp { jobId }
            AppEventBus->>passwordUtils: UnlockApp event received
            passwordUtils->>AppEventBus: Unsubscribe
            passwordUtils-->>Component: Resolve after 100ms
        end
    end
```

```mermaid
sequenceDiagram
    participant ServicePassword
    participant ServiceApp
    participant AppEventBus

    ServicePassword->>ServiceApp: dispatchUnlockJob()
    ServiceApp->>AppEventBus: Emit UnlockApp event(s) for queued jobId(s)
```

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8c05fef83f59e458c13322fd34c067356d5336cf and bb8f6314d93735116deef6f04b6e8e1c571f582b.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/kit/src/utils/passwordUtils.ts` (2 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: lint (20.x)
* GitHub Check: unittest (20.x)
* GitHub Check: Analyze (javascript-typescript)

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>packages/kit/src/utils/passwordUtils.ts (1)</summary>

`1-8`: **Imports look good for the new functionality.**

The new imports correctly support the unlock event handling mechanism.

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=OneKeyHQ/app-monorepo&utm_content=7465):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` or `Summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR