Skip to content

[fix] iOS dictation: the mic permission prompt survives the hand-off, and recent foregrounding buys a no-jump start - #281

Merged
YJack0000 merged 1 commit into
mainfrom
fix/ios-dictation-permission-linger
Aug 20, 2026
Merged

YJack0000 merged 1 commit into
mainfrom
fix/ios-dictation-permission-linger

Conversation

@YJack0000

Copy link
Copy Markdown
Contributor

What this changes

Two fixes to the keyboard-dictation hand-off, both live on ios-v1.4:

  1. First-run permission no longer eats itself. DictationView used to headline "Swipe back to your app" while the system microphone prompt was still up (state == .starting). Obeying it backgrounded the app, which cancels the prompt as a refusal — and the follow-up tap reached the app over the Darwin channel in the background, where the prompt can't be shown at all, so requestRecordPermission() failed and the keyboard was told "Turn it on in Settings" for a permission the user was never actually asked for. Now:
    • launch() reads the standing grant first; an undetermined grant asks under a new awaitingMicPermission state whose on-screen guidance is "Allow microphone access", not "swipe back".
    • The Darwin start observer declines requests it cannot serve (not granted + backgrounded), so the keyboard's parley://dictate fallback brings the app forward where the prompt can be seen.
    • Error copy distinguishes denied (Settings trip) from never answered (tap to try again).
  2. The 30 s background linger arms on every didEnterBackground, not only after a dictation session. The most common beat — open Parley, switch to the app you're typing in, tap the keyboard's mic — always found the process suspended and bounced the user through the app; now any recent foregrounding of Parley buys the next tap its no-jump window. (30 s is the platform ceiling for a background task; beyond it the URL round trip remains the only wake path.)

Why

Field report (via Jack): tapping the keyboard's mic always jumps to Parley even when the app was just open, and after coming back the keyboard claims there is no microphone permission. The permission claim traced to the prompt/guidance race above; the constant jumping traced to the linger only ever arming after a session, which first-time and casual flows never reach.

How it was verified

  • cd ios/ParleyKit && swift test — 20 tests pass
  • xcodegen generate + xcodebuild -scheme Parley -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build — app + keyboard extension build clean
  • New user-facing strings added to Localizable.xcstrings with both en and zh-Hant entries
  • Device pass (prompt/backgrounding behaviour needs real hardware; will ride the next TestFlight build)

(Desktop checkboxes — tsc/vitest/tauri — not applicable: iOS-only change.)

Screenshots

The only UI change is the DictationView headline while the system mic prompt is up ("Allow microphone access" + explainer instead of "Swipe back to your app"). The state exists only under a live, undetermined permission prompt with a signed-in account, which the screenshot demo harness can't fake — copy change is in DictationView.swift:110.

…regrounding buys a no-jump dictation start

Two dictation reports, one first-run trap and one missing wake window:

**"It says I have no microphone permission" — for a permission never
asked.** On the first-ever dictation the app opens via parley://dictate and
requestRecordPermission() puts up the system prompt — while DictationView,
seeing state == .starting, headlines "Swipe back to your app". Obeying it
backgrounds the app, which cancels the prompt as a refusal; the grant stays
undetermined. The failed session arms the 30 s linger, so the next mic tap
reaches the app over the Darwin channel in the background — where the prompt
cannot be shown at all — and requestRecordPermission() fails again, this time
publishing "Turn it on in Settings" to the keyboard for a permission the
user was never actually asked for.

Three changes close the loop:

- launch() reads the standing grant first. Undetermined asks with a new
  awaitingMicPermission flag that swaps the dictation screen's guidance from
  "swipe back" to "allow the prompt" while the alert is up. A refusal in
  the prompt gets the Settings message; a prompt dismissed unanswered gets
  "tap the mic to try again", because the next tap will simply ask again.
- The Darwin start path declines requests it cannot serve: anything short of
  .granted while backgrounded stays silent, so the keyboard's URL fallback
  brings the app forward where the prompt can actually be seen.
- The error copy distinguishes denied from never-asked.

**Every mic tap bounced through the app, even seconds after leaving it.**
The no-jump Darwin start only works while this process is awake, and the
linger that keeps it awake only armed after a dictation session ended. The
most common beat — open Parley, switch to the app you're typing in, tap the
keyboard's mic — always found the process suspended. The linger now arms on
every didEnterBackground (and releases on didBecomeActive), so any recent
foregrounding of Parley buys the next tap its ~30 s no-jump window. Thirty
seconds is the platform's ceiling for a background task; past it the URL
round trip remains the only way to wake the app.

Verified: swift test (20), xcodebuild Debug for iPhone 17 Pro simulator
(app + keyboard). New strings carry zh-Hant entries.
@YJack0000
YJack0000 merged commit d69f0c9 into main Aug 20, 2026
@sonarqubecloud

Copy link
Copy Markdown

@YJack0000 YJack0000 mentioned this pull request Aug 20, 2026
2 tasks
YJack0000 added a commit that referenced this pull request Aug 20, 2026
One fix over 1.4: the first-run microphone permission prompt is no longer
cancelled by the dictation screen's own swipe-back guidance, a backgrounded
app no longer answers keyboard starts it cannot serve with a phantom
permission error, and any recent foregrounding of Parley now buys the
keyboard's next mic tap its ~30 s no-jump window (#281).

1.4.1 rather than build 11 of 1.4 because the release workflow requires the
tag to equal CFBundleShortVersionString and ios-v1.4 is already spent.
Lanznx added a commit that referenced this pull request Aug 20, 2026
`project.yml` reached 1.4.1 and both tags went out, but `AppStore/metadata`
still stopped at 1.3 — so the version about to be submitted has no What's New
copy in either locale, and nothing tells a user why the globe key moved.

1.3 is what the App Store is serving, so these notes cover everything since it:
the voice pane becoming a control panel (#266), the recording holding on to the
microphone across foreground changes and the double-recording fix (#272), and
the permission prompt surviving the keyboard hand-off (#281).

The globe paragraph is written to be read by someone who read 1.3's. 1.3
announced the key on every device; this took it back where iOS already draws
one, and saying so plainly is cheaper than letting people think it vanished.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lanznx added a commit that referenced this pull request Aug 20, 2026
`project.yml` reached 1.4.1 and both tags went out, but `AppStore/metadata`
still stopped at 1.3 — so the version about to be submitted has no What's New
copy in either locale, and nothing tells a user why the globe key moved.

1.3 is what the App Store is serving, so these notes cover everything since it:
the voice pane becoming a control panel (#266), the recording holding on to the
microphone across foreground changes and the double-recording fix (#272), and
the permission prompt surviving the keyboard hand-off (#281).

The globe paragraph is written to be read by someone who read 1.3's. 1.3
announced the key on every device; this took it back where iOS already draws
one, and saying so plainly is cheaper than letting people think it vanished.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant