モバイル実装ガイド
このガイドでは、CDKウィジェットをモバイルアプリケーションに統合するためのベストプラクティスについて説明します。Webでは、ウィジェットは通常iframeに埋め込まれますが、モバイルアプリではウィジェットURLを効果的に表示するために異なるアプローチが必要です。
概要
CDKウィジェットは、年齢ゲート、VPC、データ通知、権限管理を含む完全なコンプライアンスフローを含むAPI呼び出しから返されるURLです。これらのウィジェットをモバイルアプリケーションに統合する場合、表示するためのいくつかのオプションがあり、それぞれ異なる機能とトレードオフがあります。主な考慮事項は次のとおりです:
- AgeKeysサポート: ユーザーが将来の確認のためにAgeKeys(FIDOベースのパスキー)を作成して使用できるかどうか
- 結果の通信: ウィジェット結果がアプリにどのように配信されるか
- ユーザーエクスペリエンス: 統合のレベルとネイティブ感
モバイル実装方法
Androidオプション
Androidは、ウィジェットURLを表示するための3つの主要な方法を提供します:
-
Chrome Custom Tabs ⭐ 推奨 - アプリのブランディングを維持するカスタマイズされたChromeブラウザタブでウィジェットURLを開きます。これにより、ユーザーをアプリのコンテキスト内に保ちながら、完全なブラウザ機能を提供します。Custom TabsはChromeとCookieと認証状態を共有し、シームレスなエクスペリエンスを可能にします。
-
WebView - AndroidのネイティブWebViewコンポーネントを使用して、アプリ内にWebコンテンツを直接埋め込みます。実装は簡単ですが、WebViewは最新のWeb標準へのサポートが限られており、特定のブラウザ機能にアクセスできません。
WebViewはAgeKeysに必要なWebAuthnをサポートしていません。WebViewを使用してウィジェットを埋め込む場合、ユーザーはAgeKeysを作成または使用できません。完全なAgeKeysサポートを備えた最適なユーザーエクスペリエンスを得るには、代わりにChrome Custom Tabsを使用してください。
- Trusted Web Activity (TWA) - 主にProgressive Web Apps向けに設計されたフルスクリーンモードでWebコンテンツを表示します。TWAは、アプリとk-IDドメイン間でデジタルアセットリンクを確立する必要があります。デジタルアセットリンクが検出されない場合、TWAは自動的にChrome Custom Tabsにフォールバックします。
Trusted Web Activitiesはウィジェット統合ではサポートされていません。k-IDのドメインでデジタルアセットリンクを設定する必要がありますが、これは利用できません。この場合、TWAはChrome Custom Tabsにフォールバックするため、より簡単な実装のためにChrome Custom Tabsを直接使用することをお勧めします。
iOSオプション
iOSは、ウィジェットURLを表示するための3つの主要な方法を提供します:
-
ASWebAuthenticationSession ⭐ 推奨 - 安全な認証フロー専用に設計されたこの方法は、システム管理のブラウザビューでWebコンテンツを表示します。SafariとCookieを共有し、WebAuthnなどの最新のWeb機能へのアクセスを提供するため、確認フローに最適です。
-
SFSafariViewController - SafariとCookieと認証状態を共有するSafariライクなインターフェースでWebコンテンツを表示します。これにより、アプリのコンテキストを維持しながら、馴染みのあるブラウジングエクスペリエンスを提供します。
-
WKWebView - アプリ内にWebコンテンツを埋め込むAppleの最新のWebビューコンポーネントです。AndroidのWebViewと同様に、WKWebViewは特定のWeb標準に制限があり、すべてのブラウザ機能にアクセスできません。
WKWebViewはAgeKeysに必要なWebAuthnをサポートしていません。WKWebViewを使用してウィジェットを埋め込む場合、ユーザーはAgeKeysを作成または使用できません。完全なAgeKeysサポートを備えた最適なユーザーエクスペリエンスを得るには、代わりにASWebAuthenticationSessionを使用してください。
AgeKeysサポートの制限
AgeKeysは、FIDOおよびWebAuthn標準に基づく再利用可能な匿名の年齢証明資格情報です。ユーザーは一度年齢を確認し、個人情報を開示することなく、異なるサービス間でその確認を再利用できます。
AgeKeysにはWebAuthnサポートが必要ですが、これはAndroid WebViewまたはiOS WKWebViewでは利用できません。これらのコンポーネントを使用してウィジェットを埋め込む場合、ユーザーは確認中にAgeKeysをオプションとして表示されず、確認成功後にAgeKeysを作成できません。
ユーザーに対してAgeKeysを有効にするには、次のいずれかの方法を使用する必要があります:
- Android: Chrome Custom Tabs
- iOS: ASWebAuthenticationSessionまたはSFSafariViewController
ウィジェット結果の受信
モバイルアプリは、ユーザーがウィジェットフローを完了した後に結果を受信する必要があります。2つのアプローチがあり、それぞれ異なる可用性があります:
コールバックURL(ユニバーサル方法)
すべての実装方法で推奨されるアプローチは、コールバックURLを使用することです。ウィジェットURLを生成するためにAPIを呼び出す際に、redirectUrlパラメータを含めます。ウィジェットフローが完了すると、ウィジェットは結果をクエリパラメータとして含めてこのURLにリダイレクトします。
コールバックURLの利点:
- すべての実装方法で動作
- DOMメッセージよりも信頼性が高い
- モバイルアプリの標準的なディープリンクパターン
- アプリがバックグラウンドに移動しても、結果は常に配信されます
コールバックURLの仕組み
- アプリにディープリンクハンドラーを登録します(例:
myapp://vpc-complete) - APIを呼び出す際に、ディープリンクを
redirectUrlとして含めます - ウィジェットは完了後にディープリンクにリダイレクトします
- アプリがディープリンクを処理し、結果を抽出します
リダイレクトは、ウィジェットURLがブラウザまたはWebビューで直接開かれた場合にのみ発生し、iframeに埋め込まれた場合は発生しません。
コールバックURLパラメータ
ウィジェットがコールバックURLにリダイレクトする際、ウィジェットタイプに関連するクエリパラメータが含まれます。例えば:
- Age Gateウィジェットには
verificationIdとresultが含まれる場合があります - セッション関連のウィジェットには
sessionIdとステータス情報が含まれる場合があります
コールバックURLの例:
myapp://vpc-complete?sessionId=608616da-4fd2-4742-82bf-ec1d4ffd8187&result=PASS
コールバックURLの実装
CDKウィジェットAPIを呼び出す際に、リクエストにredirectUrlを含めます。URLは次のいずれかになります:
- HTTPS URL:
https://example.com/vpc-complete - カスタムディープリンク:
myapp://vpc-complete
すべてのCDKウィジェットエンドポイントがまだredirectUrlをサポートしているわけではありません。可用性については、特定のAPIエンドポイントのドキュメントを確認してください。redirectUrlが利用できない場合、WebViewまたはWKWebViewでDOMメッセージを使用してください。
DOMメッセージ(WebView/WKWebViewのみ)
Android WebViewまたはiOS WKWebViewを使用する場合、ウィジェットから送信されるJavaScriptメッセージをリッスンできます。これにより、以下が可能になります:
- ウィジェット結果をリアルタイムで受信
- Webビューを閉じるタイミングを制御
- ウィジェットイベントに基づいてアプリのUIを更新
CDKウィジェットは、ウィジェットタイプに応じて異なるDOMイベントを発行します:
- End-to-Endウィジェット:
Widget.AgeGate.Challenge、Widget.AgeGate.Result - Age Gateウィジェット:
Widget.AgeGate.Challenge、Widget.AgeGate.Result - Data Noticesウィジェット:
Widget.DataNotices.ConsentApproved - Session Permissionsウィジェット:
Widget.ExitReview
DOMメッセージは、ネイティブコードでインターセプトできるpostMessageイベントとして送信されます。利用可能なイベントの詳細については、DOMイベントの概要を参照してください。
DOMメッセージはWebViewとWKWebViewでのみ機能します。Chrome Custom Tabs、Trusted Web Activity、ASWebAuthenticationSession、またはSFSafariViewControllerでは利用できません。
方法の比較
| 方法 | プラットフォーム | AgeKeys | DOMメッセージ | コールバックURL | 最適な用途 |
|---|---|---|---|---|---|
| WebView | Android | ❌ | ✅ | ✅ | 推奨されません(AgeKeysサポートなし) |
| Chrome Custom Tabs | Android | ✅ | ❌ | ✅ | ほとんどのユースケース(推奨) |
| Trusted Web Activity | Android | ✅ | ❌ | ✅ | 推奨されません(デジタルアセットリンクが必要) |
| WKWebView | iOS | ❌ | ✅ | ✅ | 推奨されません(AgeKeysサポートなし) |
| ASWebAuthenticationSession | iOS | ✅ | ❌ | ✅ | ほとんどのユースケース(推奨) |
| SFSafariViewController | iOS | ✅ | ❌ | ✅ | Safariライクなエクスペリエンス |
推奨実装
Android: Chrome Custom Tabs
機能とユーザーエクスペリエンスの最適なバランスを得るために、コールバックURLと共にChrome Custom Tabsを使用します。
Chrome Custom Tabsを選ぶ理由:
- WebAuthnによる完全なAgeKeysサポート
- すべての最新Web機能へのアクセス
- アプリブランディングによるシームレスなユーザーエクスペリエンス
- 信頼性の高いコールバックメカニズム
- Chromeと認証状態を共有
実装手順:
- コールバックURLのディープリンクハンドラーを登録します
- APIリクエストに
redirectUrlを含めます(エンドポイントがサポートしている場合) - Chrome Custom Tabsを使用してウィジェットURLを開きます
- ウィジェット結果でディープリンクコールバックを処理します
iOS: ASWebAuthenticationSession
安全でネイティブ感のあるコンプライアンスフローのために、コールバックURLと共にASWebAuthenticationSessionを使用します。
ASWebAuthenticationSessionを選ぶ理由:
- WebAuthnによる完全なAgeKeysサポート
- すべての最新Web機能へのアクセス
- システム管理のセキュリティUI
- SafariとCookieを共有
- 信頼性の高いコールバックメカニズム
実装手順:
- コールバックURLのURLスキームハンドラーを登録します
- APIリクエストに
redirectUrlを含めます(エンドポイントがサポートしている場合) - ASWebAuthenticationSessionを使用してウィジェットURLを表示します
- ウィジェット結果でURLスキームコールバックを処理します
完全な実装例
推奨アプローチを実装するためのステップバイステップの例と完全なコードサンプルを以下に示します:
ステップ1: ディープリンクハンドラーの登録
- iOS (Swift)
- Android (Kotlin)
Info.plistにURLスキームを登録します:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
AndroidManifest.xmlにインテントフィルターを追加します:
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="vpc-complete" />
</intent-filter>
</activity>
ステップ2: コールバック付きウィジェットURLの生成
- iOS (Swift)
- Android (Kotlin)
import Foundation
func generateWidgetUrl(completion: @escaping (URL?) -> Void) {
guard let url = URL(string: "https://game-api.k-id.com/api/v1/widget/generate-e2e-url") else {
completion(nil)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer YOUR_API_KEY", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestBody: [String: Any] = [
"jurisdiction": "US-CA",
"options": [
"redirectUrl": "myapp://vpc-complete"
]
]
guard let httpBody = try? JSONSerialization.data(withJSONObject: requestBody) else {
completion(nil)
return
}
request.httpBody = httpBody
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(nil)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode),
let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let widgetUrlString = json["url"] as? String,
let widgetUrl = URL(string: widgetUrlString) else {
completion(nil)
return
}
completion(widgetUrl)
}.resume()
}
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.IOException
fun generateWidgetUrl(callback: (String?) -> Unit) {
val client = OkHttpClient()
val mediaType = "application/json".toMediaType()
val requestBody = JSONObject().apply {
put("jurisdiction", "US-CA")
put("options", JSONObject().apply {
put("redirectUrl", "myapp://vpc-complete")
})
}.toString().toRequestBody(mediaType)
val request = Request.Builder()
.url("https://game-api.k-id.com/api/v1/widget/generate-e2e-url")
.post(requestBody)
.addHeader("Authorization", "Bearer YOUR_API_KEY")
.addHeader("Content-Type", "application/json")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
callback(null)
return
}
response.body?.use { body ->
val responseBody = body.string()
try {
val jsonResponse = JSONObject(responseBody)
val widgetUrl = jsonResponse.optString("url", null)
callback(widgetUrl)
} catch (e: Exception) {
callback(null)
}
} ?: callback(null)
}
override fun onFailure(call: Call, e: IOException) {
callback(null)
}
})
}
ステップ3: ウィジェットの表示
- iOS (Swift)
- Android (Kotlin)
import AuthenticationServices
// セッションをプロパティとして保存して、解放を防ぐ
var authSession: ASWebAuthenticationSession?
func displayWidget(widgetUrl: URL) {
authSession = ASWebAuthenticationSession(
url: widgetUrl,
callbackURLScheme: "myapp"
) { callbackURL, error in
if let error = error {
// エラーを処理(ユーザーがキャンセルした場合など)
return
}
if let callbackURL = callbackURL {
handleWidgetCallback(callbackURL)
}
}
authSession?.presentationContextProvider = self
authSession?.start()
}
import android.app.Activity
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
fun displayWidget(activity: Activity, widgetUrl: String?) {
if (widgetUrl == null) {
// エラーを処理:ウィジェットURLの生成に失敗
return
}
val builder = CustomTabsIntent.Builder()
val customTabsIntent = builder.build()
val uri = Uri.parse(widgetUrl)
customTabsIntent.launchUrl(activity, uri)
}
ステップ4: コールバックの処理
- iOS (Swift)
- Android (Kotlin)
func handleWidgetCallback(_ callbackURL: URL) {
guard let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems else {
return
}
let sessionId = queryItems.first(where: { $0.name == "sessionId" })?.value
let verificationId = queryItems.first(where: { $0.name == "verificationId" })?.value
let result = queryItems.first(where: { $0.name == "result" })?.value
// ウィジェット結果に基づいてUIを更新
if result == "PASS" {
// 成功したフローを処理
} else if result == "FAIL" {
// 失敗したフローを処理
}
// オプション:CDK APIエンドポイントを使用してサーバー側で確認
if let sessionId = sessionId {
verifyResultServerSide(sessionId: sessionId)
}
}
import android.content.Intent
import android.net.Uri
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent) // 重要:新しいインテントが使用されるようにする
val data: Uri? = intent.data
if (data != null && data.scheme == "myapp" && data.host == "vpc-complete") {
val sessionId = data.getQueryParameter("sessionId")
val verificationId = data.getQueryParameter("verificationId")
val result = data.getQueryParameter("result")
// ウィジェット結果に基づいてUIを更新
if (result == "PASS") {
// 成功したフローを処理
} else {
// 失敗したフローを処理
}
// オプション:CDK APIエンドポイントを使用してサーバー側で確認
if (sessionId != null) {
verifyResultServerSide(sessionId)
}
}
}
CDKウィジェットタイプ
CDKは、特定のコンプライアンスワークフロー向けに設計されたいくつかのウィジェットタイプを提供します:
- End-to-Endウィジェット: 年齢ゲート、確認、データ通知、権限、設定を含む完全なVPCフローを単一のインターフェースで処理します
- Age Gateウィジェット: ユーザーの年齢を収集し、必要に応じてVPCチャレンジをトリガーします
- Data Noticesウィジェット: 管轄区域に適したデータ通知を表示し、ユーザーの同意を収集します
- Session Permissionsウィジェット: ユーザーと保護者がアクティブなセッション権限を表示および更新できるようにします
各ウィジェットタイプとその特定のユースケースの詳細については、CDKドキュメントのガイドを参照してください。