パーミッション要求詳細 Permission requests
OS ネイティブ許可(user_device_permissions)とユーザーの同意の意思(user_consents)は
別概念です。本ページではそれぞれの意味、要求タイミング、プラットフォーム差分、
サーバー同期 API、および拒否時のフォールバック動作を定義します。
OS-level native permission (user_device_permissions) and the user's expressed
consent (user_consents) are distinct concepts. This page defines their meaning,
request timing, platform differences, server-sync APIs, and fallback behaviour when denied.
6.1 OS 許可 vs 同意 — 概念整理 6.1 OS permission vs consent — concept overview
| 概念Concept | DBテーブルDB table | 意味Meaning | 変更主体Changed by |
|---|---|---|---|
| OS ネイティブ許可OS native permission | user_device_permissions |
OS が現在付与している権限の事実(granted / denied / restricted 等)The current factual state of OS-granted permissions (granted / denied / restricted, etc.) | OS ダイアログ / 設定アプリOS dialog / Settings app |
| ユーザー同意User consent | user_consents |
ユーザーが Parky に対して「〜してよい」と意思表示した記録A record of the user's expressed willingness to allow Parky to perform a specific action | アプリ内同意ダイアログIn-app consent dialog |
OS が許可していてもユーザーが同意していない場合、アプリは機能を使いません。 逆に OS が拒否していれば、同意の有無に関わらず機能は動作しません。 両テーブルを組み合わせて判断します。
Even if the OS has granted a permission, the app will not use it unless the user has also given consent. Conversely, if the OS has denied it, the feature will not work regardless of consent. Both tables must be consulted together.
6.2 要求タイミング 6.2 Request timing
パーミッション要求は、オンボーディングの第 2 段階(認証完了直後)に集約します。 ユーザーが目的を理解した後に要求することで許可率を高めます。
Permission requests are concentrated at onboarding stage 2 (immediately after authentication completes). Requesting after the user understands the purpose increases grant rates.
| タイミングTiming | パーミッションPermission | 理由Rationale |
|---|---|---|
| オンボーディング第 2 段階(認証完了直後)Onboarding stage 2 (post-auth) | 位置情報 (When In Use)Location (When In Use) | 周辺駐車場の検索に必要と説明した直後に要求Requested immediately after explaining it is needed for nearby parking search |
| オンボーディング第 2 段階(位置情報許可直後)Onboarding stage 2 (after location) | プッシュ通知Push notification | 料金アラート・駐車終了通知の価値を伝えた後に要求Requested after explaining value of fee alerts and parking end notifications |
| 駐車開始時(初回)First parking start | 位置情報 (Always) — 昇格要求Location (Always) — elevation request | バックグラウンドでの Live Activity 精度向上のためNeeded for improved Live Activity accuracy in background |
6.3 位置情報 (Location) 6.3 Location
6.3.1 初期要求 — When In Use 6.3.1 Initial request — When In Use
Permission.locationWhenInUse.request() を呼び出します。
OS ダイアログ完了後、結果に関わらず以下の 2 API を順次呼び出します:
Calls Permission.locationWhenInUse.request(). After the OS dialog completes,
regardless of result, the following 2 APIs are called in sequence:
POST /v1/mobile/actions/permissions/record— OS が返したネイティブ許可状態と同意の意思を 1 リクエストで記録({ code, granted, recorded_at }、Idempotency-Key 必須)Records both the native permission state and consent intent in one request ({ code, granted, recorded_at }, Idempotency-Key required)
PUT /v1/me/device-permissions + POST /v1/me/consents の 2 段は web/portal 系 endpoint として残るが、
モバイルからは叩かない。Flutter は POST /v1/mobile/actions/permissions/record 1 本で device-permissions + consents の両方を BFF が同時 upsert する。
実装は api/src/bff/mobile/actions/permissions/record.ts。
The legacy PUT /v1/me/device-permissions + POST /v1/me/consents pair survives as a web/portal endpoint but
must not be called from mobile. Flutter posts POST /v1/mobile/actions/permissions/record once and the BFF
upserts both device-permissions and consents atomically. Implementation: api/src/bff/mobile/actions/permissions/record.ts.
6.3.2 昇格要求 — Always (駐車開始時) 6.3.2 Elevation request — Always (at parking start)
ユーザーが初めて駐車を開始しようとしたとき、
Permission.locationAlways.request() を呼び出します(iOS / Android 10+ 必須)。
既に Always 許可済みの場合はスキップします。
When the user starts parking for the first time, Permission.locationAlways.request()
is called (required for iOS / Android 10+). Skipped if Always is already granted.
6.3.3 プラットフォーム差分 6.3.3 Platform differences
| プラットフォームPlatform | 挙動Behaviour | permission_handler |
|---|---|---|
| iOS 14+ | 「常に許可」は When In Use 許可後に別途ダイアログ。requestAlwaysAuthorization は 1 度しか出ない"Always Allow" requires a separate dialog after "When In Use"; requestAlwaysAuthorization shows only once |
Permission.locationAlways |
| Android 10+ | ACCESS_BACKGROUND_LOCATION は ACCESS_FINE_LOCATION 付与後に別リクエスト必要。Android 11+ は設定アプリへ誘導のみACCESS_BACKGROUND_LOCATION requires a separate request after ACCESS_FINE_LOCATION; Android 11+ directs to Settings only |
Permission.locationAlways |
| Android 9 以下 | ACCESS_FINE_LOCATION のみで常時許可と同等ACCESS_FINE_LOCATION alone is equivalent to Always permission |
Permission.location |
6.3.4 拒否時 degraded mode 6.3.4 Degraded mode on denial
- 住所テキスト検索: Mapbox Geocoding API で住所入力から座標取得
- Address text search: Obtain coordinates from address input via Mapbox Geocoding API
- マニュアル選択: マップをドラッグして中心座標を指定
- Manual selection: Drag the map to set the center coordinate
- 周辺一覧 UI: 現在地ボタンを非表示にし、「位置情報が無効です」バナーを表示
- Nearby list UI: Hide the current-location button and show a "Location is disabled" banner
- Live Activity は位置情報なしでも動作するが精度が低下する
- Live Activity operates without location but with reduced precision
6.4 プッシュ通知 (Push Notification) 6.4 Push notification
6.4.1 iOS 6.4.1 iOS
iOS では Permission.notification.request() を明示的に呼ばないと
OS ダイアログが表示されません(デフォルトで通知は無効)。
FCM の getToken() 呼び出し前に許可を得ておく必要があります。
On iOS, Permission.notification.request() must be called explicitly;
the OS dialog does not appear by default (notifications are off by default).
Permission must be obtained before calling FCM's getToken().
6.4.2 Android 6.4.2 Android
| バージョンVersion | 対応Handling |
|---|---|
| Android 13+ (API 33+) | POST_NOTIFICATIONS パーミッションが必要。Permission.notification.request() で明示要求必須POST_NOTIFICATIONS is required. Must explicitly request via Permission.notification.request() |
| Android 12 以下 | AndroidManifest.xml の宣言のみで通知チャンネルが有効になる。ランタイム要求不要Notification channel is enabled by manifest declaration alone. No runtime request needed |
6.4.3 拒否時 degraded mode 6.4.3 Degraded mode on denial
- in-app 通知:
user_notificationsテーブルに蓄積し、アプリ内通知センターで表示 - In-app notifications: Accumulated in
user_notificationstable and shown in the in-app notification centre - メール通知:
user_notification_prefsのemail_enabledフラグに従い Resend 経由で送信 - Email notifications: Sent via Resend according to the
email_enabledflag inuser_notification_prefs - ユーザーが後から設定画面で通知を再有効化した場合、
AppSettings.openAppSettings()で OS 設定画面へ誘導 - If the user later re-enables notifications from app settings, guide them to OS Settings via
AppSettings.openAppSettings()
6.5 サーバー同期 API 6.5 Server sync APIs
モバイルからは POST /v1/mobile/actions/permissions/record 1 本 で送る。
BFF が user_device_permissions(OS 許可状態)と user_consents(同意の意思)を同時 upsert する。
web/portal が直接叩く下記 2 endpoint はモバイル経路では使用しない。
Mobile sends a single POST /v1/mobile/actions/permissions/record.
The BFF upserts both user_device_permissions (OS state) and user_consents (intent) atomically.
The two web/portal endpoints below are not used by the mobile path.
POST /v1/mobile/actions/permissions/record (モバイル正規ルート)(mobile canonical route)
Idempotency-Key 必須。Body: { code, granted, recorded_at? }。レスポンスは更新後 PermissionsData を含む ActionEnvelope(NAVIGATION_REFRESH_CURRENT)。
Idempotency-Key required. Body: { code, granted, recorded_at? }. Response is an ActionEnvelope (NAVIGATION_REFRESH_CURRENT) wrapping the refreshed PermissionsData.
| code | 意味 / Meaning |
|---|---|
location | 位置情報(when_in_use / always は端末側ステートマシンで判定)Location (when_in_use / always tracked on-device) |
notification | プッシュ通知Push notification |
camera | カメラCamera |
gallery | フォトライブラリPhoto library |
PUT /v1/me/device-permissions (web/portal 用、モバイル不使用)(web/portal only — not for mobile)
OS ネイティブ許可状態を user_device_permissions テーブルに UPSERT します。
UPSERTs the OS native permission state into the user_device_permissions table.
| フィールドField | 型Type | 説明Description |
|---|---|---|
permission_type |
string |
"location_when_in_use" / "location_always" / "push_notification" |
status |
string |
"granted" / "denied" / "restricted" / "limited" / "provisional" |
platform |
string |
"ios" / "android" |
os_version |
string |
OS バージョン文字列(例: "17.4.1")OS version string (e.g. "17.4.1") |
POST /v1/me/consents (web/portal 用、モバイル不使用)(web/portal only — not for mobile)
ユーザーの同意の意思を user_consents テーブルに INSERT します。
INSERTs the user's expressed consent into the user_consents table.
| フィールドField | 型Type | 説明Description |
|---|---|---|
consent_type |
string |
"location_when_in_use" / "location_always" / "push_notification" |
granted |
boolean |
ユーザーが同意した場合 true、拒否した場合 falsetrue if the user consented, false if they declined |
version |
string |
同意文書バージョン(例: "2026-01")Consent document version (e.g. "2026-01") |
6.6 パーミッション要求フロー 6.6 Permission request flow
sequenceDiagram
participant U as ユーザー
participant App as モバイルアプリ
participant OS as OS
participant BFF as Workers BFF (mobile permissions actions)
Note over App: オンボーディング第 2 段階
App->>U: 位置情報の利用目的を説明するモーダル
U->>App: 「許可する」ボタンをタップ
App->>OS: Permission.locationWhenInUse.request()
OS-->>U: OS ネイティブダイアログ
U->>OS: 「Appの使用中は許可」を選択
OS-->>App: PermissionStatus.granted
App->>BFF: POST /record { code: "location", granted: true }
Idempotency-Key:
BFF-->>App: ActionEnvelope { data: PermissionsData, navigation: refresh_current }
App->>U: プッシュ通知の利用目的を説明するモーダル
U->>App: 「許可する」ボタンをタップ
App->>OS: Permission.notification.request()
OS-->>U: OS ネイティブダイアログ
U->>OS: 「許可」を選択
OS-->>App: PermissionStatus.granted
App->>BFF: POST /record { code: "notification", granted: true }
Idempotency-Key:
BFF-->>App: ActionEnvelope { data: PermissionsData, navigation: refresh_current }
App->>U: メイン画面 (/main) へ遷移
6.7 位置情報昇格要求フロー(駐車開始時) 6.7 Location elevation flow (at parking start)
sequenceDiagram
participant U as ユーザー
participant App as モバイルアプリ
participant OS as OS
participant BFF as Workers BFF
U->>App: 「駐車開始」ボタンをタップ
App->>App: locationAlways の現在状態を確認
alt 未許可
App->>U: バックグラウンド位置情報の利用目的を説明するモーダル
U->>App: 「許可する」をタップ
App->>OS: Permission.locationAlways.request()
OS-->>U: OS ネイティブダイアログ
U->>OS: 「常に許可」を選択
OS-->>App: PermissionStatus.granted
App->>BFF: POST /v1/mobile/actions/permissions/record
{ code: "location", granted: true }
BFF-->>App: ActionEnvelope (PermissionsData)
else 拒否済み
App->>U: degraded mode の説明 + 設定アプリへのリンク
App->>App: whenInUse のみで続行
end
App->>BFF: POST /v1/mobile/actions/sessions/start
{ parking_lot_id, start_lat, start_lng }
BFF-->>App: ActionEnvelope (session, navigation: parking_session/{id})
6.8 iOS Info.plist / Android AndroidManifest.xml 宣言一覧 6.8 iOS Info.plist / Android AndroidManifest.xml declarations
iOS — Info.plist
| Key | 目的メッセージ例Purpose string example | 必要な場面Required when |
|---|---|---|
NSLocationWhenInUseUsageDescription |
「周辺の駐車場を検索するために現在地を使用します」"We use your location to find nearby parking lots." | 常時必要(When In Use 要求時)Always required (When In Use request) |
NSLocationAlwaysAndWhenInUseUsageDescription |
「駐車中のバックグラウンド追跡と Live Activity の精度向上のため常時位置情報を使用します」"We use your location in the background to maintain Live Activity accuracy while you're parked." | Always 要求時(iOS 11+)When requesting Always (iOS 11+) |
UIBackgroundModes: location |
— | バックグラウンド位置情報取得時When acquiring location in the background |
Android — AndroidManifest.xml
| Permission | 目的Purpose | 必要な API レベルRequired API level |
|---|---|---|
ACCESS_FINE_LOCATION |
高精度 GPS(駐車場ピン表示・駐車中追跡)High-accuracy GPS (lot pin display, parking tracking) | API 1+ |
ACCESS_COARSE_LOCATION |
低精度位置情報(周辺一覧のフォールバック)Low-accuracy location (fallback for nearby list) | API 1+ |
ACCESS_BACKGROUND_LOCATION |
バックグラウンド位置情報(Live Activity 精度向上)Background location (Live Activity accuracy) | API 29+ (Android 10+) |
POST_NOTIFICATIONS |
プッシュ通知の送信Send push notifications | API 33+ (Android 13+) |
6.9 拒否後の再要求ロジック 6.9 Re-request logic after denial
OS は 2 度目の要求ダイアログを表示しません(iOS: permanentlyDenied、Android: shouldShowRequestPermissionRationale == false)。
再許可には OS 設定アプリへの誘導が必要です。
The OS will not show a second-time dialog (iOS: permanentlyDenied;
Android: shouldShowRequestPermissionRationale == false).
Re-granting requires directing the user to OS Settings.
-
検出:
permission_handlerのPermissionStatus.permanentlyDeniedを確認 -
Detection: check for
PermissionStatus.permanentlyDeniedfrompermission_handler -
誘導:
AppSettings.openAppSettings()で OS 設定画面を開くボタンをアプリ内に表示 -
Direction: show an in-app button that calls
AppSettings.openAppSettings()to open OS Settings -
戻り時の再チェック:
WidgetsBindingObserver.didChangeAppLifecycleStateでAppLifecycleState.resumedを検知し、許可状態を再確認してPOST /v1/mobile/actions/permissions/recordを再送(Idempotency-Key で重複排除) -
Re-check on return: detect
AppLifecycleState.resumedviaWidgetsBindingObserver.didChangeAppLifecycleState, re-check permission state, and re-postPOST /v1/mobile/actions/permissions/record(deduped by Idempotency-Key)