firebase_pnv
Verify a phone number without ever sending an SMS.
firebase_pnv is an unofficial Flutter bridge to Google's Firebase Phone
Number Verification (PNV) SDK. Unlike firebase_auth's classic phone
sign-in - which texts the user a one-time SMS code they must type back in -
Firebase PNV verifies the number directly with the user's mobile carrier
in the background, via the Carrier API exposed through the Android
Credential Manager. The user simply taps "Allow" on a native consent sheet;
no code is ever sent or typed.
firebase_pnv (this package) |
firebase_auth phone sign-in |
|
|---|---|---|
| Verification method | Carrier / SIM (Carrier API) | SMS OTP |
| User action | Tap "Allow" on a system sheet | Read SMS, type 6-digit code |
| SMS pumping fraud risk | None - no SMS is ever sent | Vulnerable to SMS pumping/toll fraud |
| Speed | Near-instant | Depends on SMS delivery |
| Platform support | Android only (today) | Android, iOS, Web |
| Auth integration | Returns a token you exchange via a custom backend for a Firebase custom auth token | Native Firebase Auth credential |
Because Firebase PNV never sends an SMS, it is immune to SMS pumping fraud, where attackers repeatedly trigger your SMS verification endpoint with junk numbers to rack up your SMS bill. It's currently Android-only; this package ships a safe iOS stub so your shared Dart code can call it on every platform and simply fall back to SMS when it's unavailable.
Screenshots
Here is how the native consent sheet and carrier verification flow look on an Android device (Realme C67 5G):
Installation
dependencies:
firebase_pnv: ^0.0.1
Android setup
- Add Firebase to your Android app if you haven't already (guide).
- In the Firebase console, enable Phone Number Verification for your project.
minSdkVersionmust be24or higher.- This package's
android/build.gradle.ktsalready declares:
You don't need to add anything to your app'simplementation(platform("com.google.firebase:firebase-bom:34.15.0")) implementation("com.google.firebase:firebase-pnv")build.gradle.
iOS
No setup required. Every method call resolves to a PlatformException with
code UNAVAILABLE, so make sure your app checks checkSupport() (which
safely returns false on iOS) before attempting verification.
Usage
This is all you need to start using the package in your app.
1. Import it and create an instance (one instance is enough for your whole app - you can reuse it anywhere):
import 'package:firebase_pnv/firebase_pnv.dart';
final firebasePnv = FirebasePnv();
2. Check if the device supports it. Call this first, every time, before trying to verify anything:
bool isSupported = await firebasePnv.checkSupport();
- Returns
true→ device/SIM can use Firebase PNV, safe to continue. - Returns
false→ not supported (or you're on iOS) - show your normal SMS verification instead.
3. If supported, verify the phone number. This is the only method that shows UI - it pops up the native consent sheet:
Map<String, dynamic>? result = await firebasePnv.getVerifiedPhoneNumber();
String? phoneNumber = result?['phoneNumber']; // e.g. "+14155550123"
String? token = result?['token']; // send this to your backend
- If the user taps "Allow", you get back the
phoneNumberand atoken. - If the user declines, or something goes wrong, it throws a
PlatformException- always wrap the call intry/catch.
4. Send the token to your backend. Your app never needs to look inside
the token - just forward it as-is. Your backend verifies it and gives you
back a Firebase custom auth token, which you use to sign the user in (see
Backend integration below for the full backend code).
That's really the whole public API - three methods total:
| Method | What it does | When to call it |
|---|---|---|
checkSupport() |
Checks if the device/SIM can use PNV. No UI, no consent needed. | Every time, before verifying. |
getVerifiedPhoneNumber() |
Shows the consent sheet and verifies the number via carrier. | Only after checkSupport() returns true. |
enableTestSession(token) |
Switches to test mode (fake phone number, no real SIM needed). | Optional - only during development. See below. |
Testing without a real SIM (test mode)
Firebase PNV supports a test mode so you can build and test the entire flow on physical devices and emulators without a billing account or a real SIM:
-
In the Firebase console, go to Security > Phone Verification > Testing and click Generate token. (Requires the Firebase Phone Number Verification Admin IAM role.)
-
In your app, call
enableTestSessiononce, before any otherfirebase_pnvcall:await FirebasePnv().enableTestSession('COPIED_TOKEN_STRING'); -
While the test session is active,
getVerifiedPhoneNumber()resolves to a fake phone number (a valid country code followed by all zeros) instead of contacting a real carrier.
Test tokens expire after 7 days. Calling enableTestSession more than once
throws a PlatformException. Remove this call entirely before shipping to
production.
Recommended architecture: PNV first, SMS fallback
The recommended pattern is to always attempt Firebase PNV first, and
fall back to firebase_auth's SMS-based verification only when PNV is
unsupported or fails:
import 'package:firebase_pnv/firebase_pnv.dart';
import 'package:firebase_auth/firebase_auth.dart';
final _firebasePnv = FirebasePnv();
Future<void> verifyPhoneNumber(String phoneNumber) async {
// 1. Cheap, consent-free capability check.
final bool supported = await _firebasePnv.checkSupport();
if (supported) {
try {
// 2. Show the native Credential Manager consent sheet.
final result = await _firebasePnv.getVerifiedPhoneNumber();
final String? verifiedNumber = result?['phoneNumber'] as String?;
final String? pnvToken = result?['token'] as String?;
// 3. Send `pnvToken` to your backend to mint a Firebase custom auth
// token (see "Backend Integration" below), then sign in:
final customToken = await sendTokenToBackend(pnvToken!);
await FirebaseAuth.instance.signInWithCustomToken(customToken);
return;
} on Exception {
// PNV failed (user declined, network error, etc.) - fall through
// to SMS-based verification below.
}
}
// 4. Fallback: standard firebase_auth SMS OTP flow.
await FirebaseAuth.instance.verifyPhoneNumber(
phoneNumber: phoneNumber,
verificationCompleted: (credential) {},
verificationFailed: (e) {},
codeSent: (verificationId, resendToken) {},
codeAutoRetrievalTimeout: (verificationId) {},
);
}
API reference
final firebasePnv = FirebasePnv();
/// Enables a test session (development only). Call once, before any other
/// firebase_pnv call. See "Testing without a real SIM" above.
await firebasePnv.enableTestSession('COPIED_TOKEN_STRING');
/// Consent-free check - use to decide whether to show a PNV button
/// or go straight to SMS verification.
bool supported = await firebasePnv.checkSupport();
/// Shows the Credential Manager consent sheet and verifies via carrier.
/// Returns { 'phoneNumber': String, 'token': String } or throws
/// PlatformException on failure/decline.
Map<String, dynamic>? result = await firebasePnv.getVerifiedPhoneNumber();
Backend integration
Firebase Authentication does not (yet) accept the Firebase PNV token
directly as a sign-in credential. Instead, your Flutter app sends the token
returned by getVerifiedPhoneNumber() to your own backend, which:
- Verifies the token's signature and claims against Firebase PNV's JWKS
endpoint (
https://fpnv.googleapis.com/v1beta/jwks,ES256). - Extracts the verified phone number from the token's
subclaim. - Uses the Firebase Admin SDK to mint a custom auth token for that
user, which the client then exchanges for a real Firebase Auth session
via
signInWithCustomToken().
Since this package uses Firebase PNV's unified
getVerifiedPhoneNumber()API (rather than manually driving Credential Manager yourself), you do not need to generate/track nonces on the client - that's only required if you implement the custom digital-credential flow.
// server.js - Node.js + Express + firebase-admin
const express = require('express');
const admin = require('firebase-admin');
const { JwtVerifier } = require('aws-jwt-verify');
admin.initializeApp();
// Find your Firebase project number on Settings > General in the console.
const FIREBASE_PROJECT_NUMBER = '123456789';
const issuer = `https://fpnv.googleapis.com/projects/${FIREBASE_PROJECT_NUMBER}`;
const audience = issuer;
const jwksUri = 'https://fpnv.googleapis.com/v1beta/jwks';
// Verifies: signature (ES256, via JWKS), issuer/audience match your
// project, and that the token has not expired.
const fpnvVerifier = JwtVerifier.create({ issuer, audience, jwksUri });
const app = express();
app.use(express.json());
app.post('/exchangePnvToken', async (req, res) => {
const { fpnvToken } = req.body;
if (!fpnvToken) return res.sendStatus(400);
try {
// 1. Verify signature, issuer, audience, and expiry.
const payload = await fpnvVerifier.verify(fpnvToken);
// 2. The verified phone number is the token's subject claim.
const verifiedPhoneNumber = payload.sub;
// 3. Look up (or create) a Firebase user for this phone number, then
// mint a custom auth token for it.
const user = await admin.auth().getUserByPhoneNumber(verifiedPhoneNumber)
.catch(() => admin.auth().createUser({ phoneNumber: verifiedPhoneNumber }));
const customToken = await admin.auth().createCustomToken(user.uid);
return res.json({ customToken });
} catch (e) {
// Signature, issuer/audience, or expiry check failed - reject the token.
return res.sendStatus(400);
}
});
app.listen(3000);
Back on the client, sign in with the returned custom token:
final customToken = await sendTokenToBackend(pnvToken);
await FirebaseAuth.instance.signInWithCustomToken(customToken);
Going to production
Test mode is great for prototyping, but before shipping you must:
- Remove the
enableTestSession(...)call from your production build. - Add your app's SHA-256 fingerprint under Settings > General in the Firebase console.
- Upgrade your project to the Blaze (pay-as-you-go) billing plan.
- In Google Cloud Console > APIs & Services > Credentials, restrict your
Android API key to include the Firebase Phone Number Verification API. If
you use API restrictions, also allow-list
com.google.android.gmswith SHA-138918a453d07199354f8b19af05ec6562ced5788, or you'll seePERMISSION_DENIEDerrors in your logs. - In the Firebase console, go to Security > Phone Verification > Production, click Upgrade to production, and complete OAuth brand verification (requires a publicly accessible privacy policy).
Full details: Upgrade to production mode.
Example app
See example/lib/main.dart for a full neobrutalist demo app that runs the
checkSupport() -> getVerifiedPhoneNumber() flow and displays the
resulting phone number/token in a copyable block for easy sharing with beta
testers.
Contributing
This is an open-source, community-maintained bridge - issues and PRs are welcome. Since Firebase PNV is a very new SDK, expect the native API surface to evolve; please open an issue if Google changes method signatures.