.NET 8 MAUIでログイン/本人確認画面を作るとき、SMSで届くワンタイムコード(OTP)をEntryに自動入力できれば、入力ミスや離脱を大きく減らせます。ただしiOSとAndroidでは「できること」が根本的に違い、同じ発想で実装すると詰みます。両OSの制約を整理し、MAUIで現実的に運用できる実装パターンをまとめます。
.NET 8 MAUIでSMSのOTP自動入力を検討する前提
まず押さえるべきは、「アプリがSMS本文を読んでOTPを取り出す」という発想が、OSや配布形態によっては成立しない(または非常にリスクが高い)という点です。特にiOSはプライバシー設計上、アプリがユーザーのSMS本文へアクセスする仕組みが用意されていません。
| 観点 | iOS(MAUI) | Android(MAUI) |
|---|---|---|
| アプリがSMS本文を直接読む | 不可(OS仕様・プライバシー制約) | 可能(ただし強い権限が必要/審査・ポリシー面の難易度が高い) |
| 「OSのOTPオートフィル」を使う | 可能(ワンタイムコード提案→タップで入力) | 可能(端末・キーボード・設定に依存) |
| ユーザー同意のもとで1通だけ取得(API) | SMS自体を読むAPIは基本なし | 可能(SMS User Consent API 等) |
| 権限なしでOTPを受け取る(API) | SMS本文は読めない(別アプローチが必要) | 可能(SMS Retriever API 等) |
結論:iOSは「SMS本文の読み取り」はできない。狙うべきはOSオートフィル
iOSでできる最適解は、アプリ側でSMS本文を解析して自動入力するのではなく、iOSの「セキュリティコード(ワンタイムコード)自動入力」が働くように画面(Entry)とメッセージ文面を整えることです。ユーザー体験としては「キーボード上部にコードが提案され、タップするとEntryに入る」挙動になります。
iOS側で狙うUX:キーボード上の「ワンタイムコード」提案
iOSの自動入力を引き出すために、アプリ側ではネイティブのテキストフィールドに対して「これはワンタイムコード入力だ」と伝える必要があります。ネイティブ(UITextField)ではtextContentTypeをoneTimeCodeに設定するのが定番です。
MAUIでiOSのone-time-codeを有効にする実装例(Handlerで設定)
MAUIはハンドラー(Handler)でプラットフォームのネイティブビューにアクセスできます。OTP専用のEntryを作って、iOSだけネイティブ側へ設定を流す形が分かりやすいです。
1) 共有プロジェクトにOTP専用Entryを用意
using Microsoft.Maui.Controls;
namespace YourApp.Controls;
public class OtpEntry : Entry
{
// 必要なら桁数や正規表現などのプロパティを追加してもOK
}
2) MauiProgramでHandlerマッピングを追加(iOS向け oneTimeCode)
using Microsoft.Maui.Handlers;
using YourApp.Controls;
#if IOS
using UIKit;
#endif
namespace YourApp;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp();
// OtpEntryに対するプラットフォーム設定
EntryHandler.Mapper.AppendToMapping("OtpEntryMapping", (handler, view) =>
{
if (view is not OtpEntry)
return;
#if IOS
// iOS:ワンタイムコード用のコンテンツタイプを設定
handler.PlatformView.TextContentType = UITextContentType.OneTimeCode;
// できれば数字キーボード(見た目・入力ミス低減)
handler.PlatformView.KeyboardType = UIKeyboardType.NumberPad;
#endif
});
return builder.Build();
}
}
3) XAMLでOtpEntryを使う
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:YourApp.Controls"
x:Class="YourApp.Views.VerifyPage">
<VerticalStackLayout Padding="24" Spacing="12">
<Label Text="SMSに届いた認証コードを入力してください" />
<controls:OtpEntry
x:Name="OtpEntry"
Keyboard="Numeric"
MaxLength="6"
Placeholder="例:123456" />
</VerticalStackLayout>
iOSのオートフィルを出しやすくするSMS文面のコツ
サーバー側(SMS送信側)の文面が雑だと、iOSがワンタイムコードとして認識しにくくなります。以下は「認識されやすい形」を目指すための実務的な観点です(厳密なルールというより成功率を上げるコツです)。
| ポイント | 推奨 | 避けたい |
|---|---|---|
| コードの表記 | 4〜8桁程度の数字を単独に近い形で | 文中に複数の数字(注文番号/電話番号/日付)が混在 |
| 説明文 | 「認証コード」「確認コード」など用途が明確 | 「番号」「ID」など曖昧な表現 |
| アプリ名 | 送信元・本文にアプリ名/サービス名が含まれる | どのアプリ向けか分からない |
| URL | 基本は最小限 | URLや短縮URL、追跡パラメータが多い |
iOSは「アプリがSMSを読む」ことはできませんが、OSの提案→タップで入力の導線を整えるだけでも体感はかなり良くなります。iOS対応をこの方向に寄せることで、プライバシーや審査の地雷を踏みにくくなります。
Androidは実装可能。ただし最初に「どの方式を選ぶか」が最重要
Androidでは技術的にはSMSを受信して本文からOTPを抽出できます。しかし、READ/RECEIVE_SMSは非常に強い(危険)権限で、ユーザーの心理的ハードルも高く、配布ストアのポリシーや審査で問題になりやすいです。
そのため実務では「権限で読む」以外の選択肢(SMS Retriever / User Consent)を先に検討し、どうしても必要な場合のみ権限方式に寄せるのが安定します。
| 方式 | SMS権限 | ユーザー操作 | 実装難易度 | 向いているケース |
|---|---|---|---|---|
| SMS Retriever API(推奨) | 不要 | 基本不要(自動で受信→抽出) | 中 | 一般向けアプリ、審査・運用の安定重視 |
| SMS User Consent API | 不要 | 「このSMSを読み取りますか?」に同意が必要 | 中 | 自動取得よりも透明性を優先したい |
| SMS_RECEIVED + 解析(RECEIVE/READ_SMS) | 必要(強い) | 権限許可が必須 | 低〜中 | 企業内配布、端末管理下、ストア制約がない環境 |
Android推奨:SMS Retriever APIでOTPを受け取る(権限なし)
権限なしでOTPの自動入力を成立させる代表例がSMS Retriever APIです。ポイントは、アプリ固有の署名ハッシュ(アプリハッシュ)をSMS本文に含めること。端末側が「このSMSはこのアプリ宛だ」と判断できるため、アプリはSMS権限なしで対象メッセージだけ受け取れます。
実装の全体像
| 手順 | やること | MAUIでの置き場所 |
|---|---|---|
| 依存関係 | Google Play services(Auth API Phone系)を利用できるようにする | Platforms/Android(NuGet追加) |
| 開始 | リスナー開始(通常はOTP入力画面表示時) | MainActivity / Androidサービス |
| 受信 | Retriever用BroadcastReceiverでメッセージを受け取る | Platforms/Android |
| 抽出 | 本文からOTP(数字)を正規表現で抽出 | Platforms/Android |
| 反映 | 共有コードへ通知し、Entry.Textを更新 | Views / ViewModel |
SMS Retrieverの受信例(BroadcastReceiver)
以下は「受け取ったSMS文字列からOTPを抜いて共有側へ通知する」骨組みです。依存パッケージや名前空間は環境で差が出るため、基本構造として捉えてください(概念が合っていれば移植できます)。
using Android.App;
using Android.Content;
using Android.Gms.Auth.Api.Phone;
using Android.Gms.Common;
using Android.Gms.Common.Apis;
using CommunityToolkit.Mvvm.Messaging;
using System.Text.RegularExpressions;
namespace YourApp.Platforms.Android;
public sealed record OtpReceivedMessage(string Code);
[BroadcastReceiver(Enabled = true, Exported = true)]
[IntentFilter(new[] { SmsRetriever.SmsRetrievedAction })]
public class SmsRetrieverReceiver : BroadcastReceiver
{
public override void OnReceive(Context context, Intent intent)
{
if (intent.Action != SmsRetriever.SmsRetrievedAction)
return;
var extras = intent.Extras;
if (extras is null) return;
var status = extras.Get(SmsRetriever.ExtraStatus) as Status;
if (status is null) return;
if (status.StatusCode == CommonStatusCodes.Success)
{
var message = extras.GetString(SmsRetriever.ExtraSmsMessage);
if (string.IsNullOrWhiteSpace(message)) return;
// 例:4〜8桁をOTPとして抽出(要件に合わせて調整)
var m = Regex.Match(message, @"\b(\d{4,8})\b");
if (!m.Success) return;
var code = m.Groups[1].Value;
WeakReferenceMessenger.Default.Send(new OtpReceivedMessage(code));
}
else if (status.StatusCode == CommonStatusCodes.Timeout)
{
// タイムアウト(一定時間で待受が切れる)
// 必要なら再開やユーザーへの案内を行う
}
}
}
OTP入力画面を表示したタイミングでRetrieverを開始
待ち受けを「常時ON」にするのではなく、OTP入力画面(確認画面)を開いたときに開始すると無駄がなく、挙動も追いやすくなります。
// Platforms/Android/MainActivity.cs の例(概念)
using Android.Gms.Auth.Api.Phone;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
// 必要になったタイミングで StartSmsRetriever を呼ぶのが理想。
// ここでは例としてOnCreateに書いているが、画面遷移時に呼ぶ設計がおすすめ。
SmsRetriever.GetClient(this).StartSmsRetriever();
}
共有側(Page / ViewModel)で受信してEntryへ反映
CommunityToolkit.MvvmのWeakReferenceMessengerを使うと、プラットフォーム側→共有側の通知が素直になります(依存を増やしたくなければイベントやDIでもOKです)。
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Maui.Dispatching;
using YourApp.Platforms.Android; // OtpReceivedMessage の参照
public partial class VerifyPage : ContentPage, IRecipient<OtpReceivedMessage>
{
public VerifyPage()
{
InitializeComponent();
WeakReferenceMessenger.Default.Register(this);
}
public void Receive(OtpReceivedMessage message)
{
// UIスレッドで更新
MainThread.BeginInvokeOnMainThread(() =>
{
OtpEntry.Text = message.Code;
// ここで自動送信するかどうかはUX次第
// 例:6桁そろったら自動検証する、など
});
}
protected override void OnDisappearing()
{
base.OnDisappearing();
WeakReferenceMessenger.Default.Unregister<OtpReceivedMessage>(this);
}
}
SMS Retrieverで成功率を上げるためのSMS文面設計
Retrieverは「このアプリ宛のSMS」だと分かる情報を本文に入れる必要があります。実運用ではSMS送信基盤側でテンプレートを用意し、OTPだけ差し替える方式が管理しやすいです。
| 項目 | 意識すること |
|---|---|
| OTPの抽出 | 本文中の数字が複数あると誤抽出するため、OTP以外の数字を極力減らす |
| アプリハッシュ | Retrieverが拾える形式にする(末尾付近に固定で入れる運用が多い) |
| メッセージ長 | 長すぎると端末側で扱いが不安定になるため、短く簡潔に |
SMS送信側が自由にいじれない(外部ベンダー固定など)場合は、次に紹介する「権限方式」や「User Consent」へ寄せた方が早いケースもあります。
Android実装例:SMS受信(BroadcastReceiver)でOTPを抽出してEntryへ自動入力
ここからは、質問で挙がりやすい「SMS受信イベント(SMS_RECEIVED)を拾って本文を解析する」方式を、MAUI向けに噛み砕いて解説します。技術的には分かりやすい一方で、強い権限を伴うため、配布形態や審査要件に注意してください。
必要な権限と役割
| 権限 | 目的 | 備考 |
|---|---|---|
android.permission.RECEIVE_SMS | SMS受信ブロードキャストを受け取る | 本文は受信インテントから取得できることが多い |
android.permission.READ_SMS | SMS受信箱(content://sms)を読み取る | 「最新SMSをクエリして読む」方式で必要。権限負担が増える |
設計としては、まずRECEIVE_SMSだけで足りる構成を目指し、どうしても必要な場合だけREAD_SMSを追加するのが無難です。
AndroidManifest.xmlに権限を追加
MAUIのAndroidマニフェストは通常Platforms/Android/AndroidManifest.xmlを編集します。
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<!-- Inboxをクエリするなら必要(可能なら避ける) -->
<uses-permission android:name="android.permission.READ_SMS" />
<application ... />
</manifest>
実行時権限の許可要求(危険権限)
Android 6.0(API 23)以降、危険権限は実行時にユーザー許可が必要です。MAUIでもMainActivity側で要求できます。
using Android.Content.PM;
using Android.OS;
using AndroidX.Core.App;
using AndroidX.Core.Content;
namespace YourApp;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true)]
public class MainActivity : MauiAppCompatActivity
{
const int RequestSmsPermissionsId = 1001;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
RequestSmsPermissionsIfNeeded();
}
void RequestSmsPermissionsIfNeeded()
{
var permissions = new[]
{
Android.Manifest.Permission.ReceiveSms,
Android.Manifest.Permission.ReadSms, // 必要な場合のみ
};
var needRequest = permissions.Any(p =>
ContextCompat.CheckSelfPermission(this, p) != Permission.Granted);
if (needRequest)
ActivityCompat.RequestPermissions(this, permissions, RequestSmsPermissionsId);
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
{
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
// Essentials系を使っている場合はPlatformへも伝搬する(環境に応じて)
// Microsoft.Maui.ApplicationModel.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
運用では、単に許可を求めるのではなく、なぜ必要なのか(OTP入力を省略してログインを簡単にする等)を事前に説明する画面を挟むと許可率が上がります。
SMS_RECEIVEDを受け取るBroadcastReceiver(本文からOTP抽出)
受信したSMS本文は、受信インテントのPDUから取得できます。MAUIではPlatforms/Android配下にReceiverクラスを置くと整理しやすいです。
using Android.App;
using Android.Content;
using Android.Provider;
using Android.Telephony;
using CommunityToolkit.Mvvm.Messaging;
using System.Text.RegularExpressions;
namespace YourApp.Platforms.Android;
public sealed record OtpReceivedMessage(string Code);
[BroadcastReceiver(Enabled = true, Exported = true, Permission = "android.permission.BROADCAST_SMS")]
[IntentFilter(new[] { Telephony.Sms.Intents.SmsReceivedAction })]
public class OtpSmsReceiver : BroadcastReceiver
{
public override void OnReceive(Context context, Intent intent)
{
if (intent.Action != Telephony.Sms.Intents.SmsReceivedAction)
return;
var messages = Telephony.Sms.Intents.GetMessagesFromIntent(intent);
if (messages is null || messages.Length == 0) return;
// 分割SMSの可能性があるため連結
var body = string.Concat(messages.Select(m => m.MessageBody ?? string.Empty));
var from = messages[0].OriginatingAddress ?? string.Empty;
// 送信元でフィルタ(任意:SMS送信元番号/アルファベット送信者IDなど)
// if (!from.Contains("YourSender")) return;
// OTP抽出(例:6桁固定。要件に応じて4〜8桁にする等)
var match = Regex.Match(body, @"\b(\d{6})\b");
if (!match.Success) return;
var code = match.Groups[1].Value;
// 共有側へ通知
WeakReferenceMessenger.Default.Send(new OtpReceivedMessage(code));
}
}
ポイントは次のとおりです。
- 分割SMSを想定し、本文を連結してから抽出する
- 送信元フィルタ(送信者ID/電話番号/キーワード)を入れて誤爆を防ぐ
- 正規表現は「ゆるすぎる」と誤抽出し、「固すぎる」と取りこぼす。実SMSで検証して調整する
「最新SMSをinboxから拾う」方式(ContentResolver.Query)の注意点
質問の流れでよくあるのが、Receiverで受信通知だけ拾って、本文はcontent://sms/inboxから最新を引く実装です。これは実装としては可能ですが、次のデメリットがあります。
- READ_SMSが必要になり、権限負担が増える
- タイミング次第で「最新」が別のSMSになるなど、誤取得リスクがある
- 端末やOEM実装差で、SMS Provider周りの挙動がブレることがある
それでも実装する場合の最小例は以下です(必ず送信元や本文パターンで絞り込む設計にしてください)。
using Android.Content;
using Android.Database;
using Android.Net;
public static class SmsInboxReader
{
public static string? GetLatestSmsBody(Context context)
{
var uri = Uri.Parse("content://sms/inbox");
var projection = new[] { "address", "body", "date" };
using ICursor? cursor = context.ContentResolver?.Query(
uri,
projection,
null,
null,
"date DESC"
);
if (cursor is null) return null;
if (!cursor.MoveToFirst()) return null;
var bodyIndex = cursor.GetColumnIndex("body");
if (bodyIndex < 0) return null;
return cursor.GetString(bodyIndex);
}
}
実務では「受信インテントから本文を取る」方がシンプルかつ安全なため、まずはReceiver内で完結する設計をおすすめします。
Entryに自動入力する際の実装ポイント(MAUI共通)
SMSからOTPを得られても、Entry.Textにそのまま入れて終わりだと、UXが伸びません。OTP入力は失敗しやすい導線なので、以下をセットで入れると完成度が上がります。
| ポイント | 理由 | 実装のヒント |
|---|---|---|
| 数字のみ入力 | 誤入力・貼り付け時の混入を防ぐ | Keyboard="Numeric"+入力検証 |
| 最大桁数 | 余計な文字を受け付けない | MaxLengthを設定(例:6) |
| 貼り付け対応 | 自動入力できない端末でも救える | ハイフンや空白を除去してから検証 |
| 自動送信の条件 | 打鍵中に送信しない | 桁数が揃ったタイミングでのみ検証 |
| タイムアウト/再送 | 待ち続けるUXを避ける | カウントダウンと再送ボタン |
貼り付け・自動入力の共通整形(数字だけ取り出す)
using System.Text.RegularExpressions;
public static class OtpNormalizer
{
public static string Normalize(string? input, int maxLen = 6)
{
if (string.IsNullOrWhiteSpace(input))
return string.Empty;
// 数字だけ抜く("123-456" や "コード: 123456" にも対応)
var digits = Regex.Replace(input, @"\D", "");
if (digits.Length > maxLen)
digits = digits.Substring(0, maxLen);
return digits;
}
}
受信側の正規表現が取りこぼしたときでも、最終的にこの正規化を通すことで「余計な文字がEntryに入る」事故を減らせます。
実運用での注意点(審査・セキュリティ・ログ)
OTPは本人確認に直結するため、実装が動けばOKではありません。特にAndroidのSMS権限方式は運用事故が起きやすいので、次の観点を強く意識してください。
- SMS本文をログに出さない(デバッグログ、クラッシュレポート、解析基盤へ流さない)
- OTPは保存しない(Preferences等へ永続化しない/必要なら短時間で破棄)
- 送信元フィルタや本文パターンで誤抽出を防ぐ(他サービスのSMSを拾う事故を避ける)
- 権限が拒否されたときの導線を必ず用意(手入力/貼り付けで完結できるようにする)
- 公開ストア配布の場合、SMS権限はポリシーや審査で制約が強い。可能ならRetriever / Consentを優先する
よくあるハマりどころ(トラブルシューティング)
AndroidでReceiverが呼ばれない
- 権限が許可されているか(設定アプリ→権限)を確認
- Android 12以降をターゲットにしている場合、Receiverの
Exported設定が不足していると受信できないことがある - インテントフィルタのアクション名が正しいか(
Telephony.Sms.Intents.SmsReceivedAction) - 端末の省電力設定でバックグラウンド制限が強い場合、想定と挙動が変わることがある(ただしOTP画面表示中に受け取る設計なら影響を減らせる)
OTPが誤って抽出される/抽出できない
- SMS本文に数字が複数ある:送信テンプレを見直し、OTP以外の数字を減らす
- 桁数が固定でない:抽出正規表現を
\d{4,8}などへ調整し、最終的に桁数チェックで弾く - 送信元が変わる:電話番号の固定が難しい場合、本文中のキーワード(サービス名)を併用する
iOSでオートフィルが出ない
UITextField側にoneTimeCodeが設定されているか(Handlerでの設定漏れ)- SMS本文が冗長すぎる/数字が多すぎる:テンプレートを短くし、OTPが目立つようにする
- 端末側の設定や学習状態にも左右される:手入力・貼り付けで完結できるUIを必ず残す
まとめ:iOSは「読まない」、Androidは「読めるが選び方が重要」
.NET 8 MAUIでSMSのOTP自動入力を狙うとき、iOSは「SMS本文を読む」のではなくOSのオートフィルが働く状態を作るのが王道です。一方Androidは複数の実装方式があり、配布形態・審査・ユーザー同意の観点で最適解が変わります。一般向けアプリで安定運用を目指すなら、まずは権限不要のRetriever/Consent系を検討し、最後の手段として権限方式を採用すると事故が減ります。

コメント