expo-app-blocker 0.1.62 → 0.1.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +56 -0
  2. package/package.json +1 -1
  3. package/src/index.ts +14 -0
package/README.md CHANGED
@@ -388,6 +388,31 @@ startMonitoring(); // Start foreground service (auto-started on init)
388
388
  stopMonitoring(); // Stop monitoring
389
389
  ```
390
390
 
391
+ #### Deep-link contract (how your app is launched)
392
+
393
+ When the foreground service detects a blocked app, it brings **your** app to the
394
+ front via a deep link using your app's own URL scheme:
395
+
396
+ ```
397
+ <yourScheme>://blocked?app=<AppName>&package=<package.name>&reason=<reason>
398
+ ```
399
+
400
+ | Param | Description |
401
+ |---|---|
402
+ | `app` | Human-readable label of the blocked app (URL-encoded), e.g. `Instagram` |
403
+ | `package` | Android package name of the blocked app, e.g. `com.instagram.android` |
404
+ | `reason` | Why the block fired — see below. Lets you branch your UI |
405
+
406
+ `reason` values:
407
+
408
+ | Value | Meaning |
409
+ |---|---|
410
+ | `opened` | A blocked app was freshly brought to the foreground. |
411
+ | `expired` | A [temporary unlock](#android-temporary-unlock) expired while the user was still **inside** the blocked app. Handle this if you want a softer "time's up" interstitial instead of jumping straight into your gate. |
412
+
413
+ Handle the deep link with `expo-linking` / `expo-router` like any other route.
414
+ Your scheme is auto-detected from the app config; no extra setup required.
415
+
391
416
  ### iOS: App Selection
392
417
 
393
418
  Two ways to let users pick which apps to block:
@@ -480,6 +505,37 @@ const seconds = getRemainingUnlockTime(); // seconds remaining
480
505
  await relockApps(); // re-lock immediately
481
506
  ```
482
507
 
508
+ ### Android: Temporary Unlock
509
+
510
+ The same temporary-unlock API works on Android. The foreground service pauses
511
+ blocking for the requested duration and auto-resumes when it expires — the timer
512
+ lives in the service, so it survives your app being backgrounded.
513
+
514
+ ```typescript
515
+ import {
516
+ temporaryUnlock,
517
+ getRemainingUnlockTime,
518
+ relockApps,
519
+ } from 'expo-app-blocker';
520
+
521
+ // Suppress blocking for N minutes; auto-resumes on expiry
522
+ await temporaryUnlock(15);
523
+ // { unlocked: true, expiresAt: number }
524
+
525
+ const seconds = getRemainingUnlockTime(); // seconds remaining, 0 if none
526
+ await relockApps(); // end the unlock now, re-block immediately
527
+ ```
528
+
529
+ | Function | Android behavior |
530
+ |---|---|
531
+ | `temporaryUnlock(minutes)` | Suppresses blocking for `minutes` (min 1, rounded). Replaces any active unlock. |
532
+ | `getRemainingUnlockTime()` | Seconds left on the active unlock, or `0`. Backed by a persisted expiry, so it's accurate without the app holding service state. |
533
+ | `relockApps()` | Ends the unlock immediately and re-blocks the foreground app on the next poll. |
534
+ | `isTemporarilyUnlocked()` | iOS only — returns `false` on Android. Use `getRemainingUnlockTime() > 0` instead. |
535
+
536
+ > When an unlock expires while the user is still inside a blocked app, the
537
+ > deep link fires with `reason=expired` (see [Deep-link contract](#deep-link-contract-how-your-app-is-launched)).
538
+
483
539
  ### iOS: Shield Button Events
484
540
 
485
541
  When a user taps the primary button on the shield overlay, your app receives an event:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-app-blocker",
3
- "version": "0.1.62",
3
+ "version": "0.1.63",
4
4
  "description": "Expo module for cross-platform app blocking. Android: UsageStatsManager + Overlay. iOS: Screen Time API (FamilyControls + ManagedSettings + DeviceActivity).",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
package/src/index.ts CHANGED
@@ -168,6 +168,13 @@ export function isAppBlocked(bundleIdentifier: string): boolean {
168
168
  // iOS-specific: Temporary unlock
169
169
  // ──────────────────────────────────────────────────────────────────────────────
170
170
 
171
+ /**
172
+ * Suppress blocking for `durationMinutes`, then auto-resume.
173
+ *
174
+ * iOS removes the Family Controls shields; Android pauses the foreground-service
175
+ * poll (the timer lives in the service, so it survives app backgrounding).
176
+ * Calling again replaces any active unlock. Android rounds to a whole minute (min 1).
177
+ */
171
178
  export async function temporaryUnlock(durationMinutes: number = 15): Promise<TemporaryUnlockResult> {
172
179
  if (Platform.OS === "android") {
173
180
  NativeModule.temporaryUnlockAndroid(Math.max(1, Math.round(durationMinutes)));
@@ -176,6 +183,7 @@ export async function temporaryUnlock(durationMinutes: number = 15): Promise<Tem
176
183
  return NativeModule.temporaryUnlock(durationMinutes);
177
184
  }
178
185
 
186
+ /** iOS only — returns `false` on Android. On Android use `getRemainingUnlockTime() > 0`. */
179
187
  export function isTemporarilyUnlocked(): boolean {
180
188
  if (Platform.OS !== "ios") return false;
181
189
  return NativeModule.isTemporarilyUnlocked();
@@ -187,6 +195,12 @@ export function getRemainingUnlockTime(): number {
187
195
  return NativeModule.getRemainingUnlockTime();
188
196
  }
189
197
 
198
+ /**
199
+ * End an active temporary unlock immediately and re-block.
200
+ *
201
+ * iOS restores the shields; Android cancels the unlock and re-blocks the
202
+ * foreground app on the next poll. Safe to call when nothing is unlocked.
203
+ */
190
204
  export async function relockApps(): Promise<RelockResult> {
191
205
  if (Platform.OS === "android") {
192
206
  NativeModule.relockAndroid();