<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: George Ikwegbu Chinedu</title>
    <description>The latest articles on DEV Community by George Ikwegbu Chinedu (@gikwegbu).</description>
    <link>https://dev.to/gikwegbu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F406889%2Fa7f171f8-0ad2-4188-a340-e07d5be63cfd.jpg</url>
      <title>DEV Community: George Ikwegbu Chinedu</title>
      <link>https://dev.to/gikwegbu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gikwegbu"/>
    <language>en</language>
    <item>
      <title>$$$$$$BACKUP FOR | Shipping a Safe Rollback: Downgrading a Drift (SQLite) Database in Flutter Without Losing User Data</title>
      <dc:creator>George Ikwegbu Chinedu</dc:creator>
      <pubDate>Thu, 22 Oct 2026 20:30:26 +0000</pubDate>
      <link>https://dev.to/gikwegbu/backup-for-shipping-a-safe-rollback-downgrading-a-drift-sqlite-database-in-flutter-3chk</link>
      <guid>https://dev.to/gikwegbu/backup-for-shipping-a-safe-rollback-downgrading-a-drift-sqlite-database-in-flutter-3chk</guid>
      <description>&lt;p&gt;You're about to ship a release that bumps your local database schema. Being careful, you also prepare a &lt;strong&gt;rollback build&lt;/strong&gt;, the version currently in production, so that if the new release has problems you can push the old one straight back out.&lt;/p&gt;

&lt;p&gt;Then you test the rollback, and it crashes on startup.&lt;/p&gt;

&lt;p&gt;This post covers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Why Drift refuses to "upgrade" to a lower schema version&lt;/li&gt;
&lt;li&gt;The fix (and why the obvious fix doesn't work)&lt;/li&gt;
&lt;li&gt;A bug that's easy to introduce while making the fix&lt;/li&gt;
&lt;li&gt;The git worktree workflow for building both versions side by side&lt;/li&gt;
&lt;li&gt;Building three test APKs so QA can test &lt;strong&gt;upgrade → rollback → re-upgrade&lt;/strong&gt; without losing data&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All names, branches and paths below are made up for the article.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Production&lt;/strong&gt; (&lt;code&gt;v1.4.1&lt;/code&gt;) uses database &lt;strong&gt;schema 11&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The new release&lt;/strong&gt; (&lt;code&gt;v1.5.0&lt;/code&gt;, branch &lt;code&gt;feature/new-release&lt;/code&gt;) moves to &lt;strong&gt;schema 12&lt;/strong&gt;. It adds three nullable columns:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;settings.dark_mode_enabled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;settings.beta_features_enabled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reports.submitted_at&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rollback build&lt;/strong&gt; (branch &lt;code&gt;release/1.4.2&lt;/code&gt;) is production code (schema 11) with a higher build number, so the stores treat it as an update.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The local database is &lt;a href="https://drift.simonbinder.eu/" rel="noopener noreferrer"&gt;Drift&lt;/a&gt; (version 2.x), and migrations use Drift's generated &lt;code&gt;stepByStep&lt;/code&gt; helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;schemaVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="n"&gt;MigrationStrategy&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;migration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MigrationStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;onCreate:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;seedDefaultSettings&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nl"&gt;onUpgrade:&lt;/span&gt; &lt;span class="n"&gt;stepByStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;from1To2:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nl"&gt;from10To11:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nl"&gt;beforeOpen:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;customStatement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'PRAGMA foreign_keys = ON'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;validateDatabaseSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// debug-only check&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;On a device that had run the schema 12 build, installing the schema 11 rollback build crashed on startup with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;StateError: runMigrationSteps was asked to downgrade from versions 12 to 11.
Migration steps can only upgrade. This might happen if users downgrade app versions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  First attempt: handle it in &lt;code&gt;beforeOpen&lt;/code&gt; (didn't work)
&lt;/h2&gt;

&lt;p&gt;My first idea was to detect the downgrade in &lt;code&gt;beforeOpen&lt;/code&gt;, then drop and recreate every table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;beforeOpen:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionBefore&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionBefore&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;schemaVersion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createMigrator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;customStatement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'PRAGMA foreign_keys = OFF'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;allTables&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;customStatement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'PRAGMA foreign_keys = ON'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It made no difference. The same error kept appearing, and the code inside the &lt;code&gt;if&lt;/code&gt; never ran.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why: Drift calls &lt;code&gt;onUpgrade&lt;/code&gt; before &lt;code&gt;beforeOpen&lt;/code&gt;, including for downgrades
&lt;/h3&gt;

&lt;p&gt;In Drift's source (&lt;code&gt;lib/src/runtime/api/db_base.dart&lt;/code&gt;), the open sequence looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wasCreated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_resolvedMigration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;migrator&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hadUpgrade&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;           &lt;span class="c1"&gt;// versionBefore != versionNow&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_resolvedMigration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onUpgrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;migrator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionBefore&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionNow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_resolvedMigration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;beforeOpen&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;hadUpgrade&lt;/code&gt; is just &lt;code&gt;versionBefore != versionNow&lt;/code&gt;. &lt;strong&gt;Drift doesn't distinguish upgrades from downgrades.&lt;/strong&gt; Both go through &lt;code&gt;onUpgrade&lt;/code&gt;, and &lt;code&gt;MigrationStrategy&lt;/code&gt;'s own docs say so: &lt;em&gt;"Schema version upgrades and downgrades will both be run here."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stepByStep&lt;/code&gt; then calls &lt;code&gt;VersionedSchema.runMigrationSteps&lt;/code&gt;, which refuses to downgrade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;StateError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'runMigrationSteps was asked to downgrade from versions &lt;/span&gt;&lt;span class="si"&gt;$from&lt;/span&gt;&lt;span class="s"&gt; to &lt;/span&gt;&lt;span class="si"&gt;$to&lt;/span&gt;&lt;span class="s"&gt;. ...'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the exception is thrown in &lt;code&gt;onUpgrade&lt;/code&gt;, the database fails to open, and &lt;code&gt;beforeOpen&lt;/code&gt; never runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why dropping everything was a bad idea anyway
&lt;/h3&gt;

&lt;p&gt;Even if it had worked, dropping every table and recreating it would have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lost unsynced data.&lt;/strong&gt; Anything the user created offline and hadn't uploaded yet would be gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logged users out&lt;/strong&gt;, if session or settings data lives in the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipped the default seed data.&lt;/strong&gt; &lt;code&gt;onCreate&lt;/code&gt; seeds default settings, but a bare &lt;code&gt;createAll()&lt;/code&gt; doesn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rollback is meant to make things safer for users, and wiping their data on the way back defeats that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the schema change actually was
&lt;/h2&gt;

&lt;p&gt;Before choosing a fix, I compared the two schemas. Schema 12 &lt;strong&gt;only added nullable columns&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That matters because Drift-generated queries always list columns explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nv"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"settings"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="nv"&gt;"reports"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An older build that doesn't know about &lt;code&gt;dark_mode_enabled&lt;/code&gt; never selects it, and on insert SQLite stores &lt;code&gt;NULL&lt;/code&gt;. &lt;strong&gt;Extra nullable columns (or columns with a default) are harmless to older code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the safest downgrade is to &lt;strong&gt;do nothing&lt;/strong&gt;: keep the data and let Drift record the lower version.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ This only works when the newer schema is &lt;em&gt;additive&lt;/em&gt;. If it renamed or dropped columns, added &lt;code&gt;NOT NULL&lt;/code&gt; columns without defaults, or changed table shapes, you'd need a real downgrade migration (for example &lt;code&gt;m.alterTable(TableMigration(...))&lt;/code&gt; to rebuild the table).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Handle the downgrade in &lt;code&gt;onUpgrade&lt;/code&gt;, before &lt;code&gt;stepByStep&lt;/code&gt; runs
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;stepByStep(...)&lt;/code&gt; returns an &lt;code&gt;OnUpgrade&lt;/code&gt; function, &lt;code&gt;(Migrator m, int from, int to) =&amp;gt; Future&amp;lt;void&amp;gt;&lt;/code&gt;. Wrap it in your own closure and check the direction first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;onUpgrade:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Drift calls onUpgrade for downgrades too (from &amp;gt; to), e.g. when a user&lt;/span&gt;
  &lt;span class="c1"&gt;// on a newer build (schema 12) installs this rollback build (schema 11).&lt;/span&gt;
  &lt;span class="c1"&gt;// stepByStep only supports upgrades and throws on a downgrade, so skip it&lt;/span&gt;
  &lt;span class="c1"&gt;// and keep the existing data. Newer schemas only added nullable columns,&lt;/span&gt;
  &lt;span class="c1"&gt;// which this version ignores. Drift then writes user_version = `to`.&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Normal upgrade path. stepByStep returns an OnUpgrade function, so it&lt;/span&gt;
  &lt;span class="c1"&gt;// must be invoked with (m, from, to) and awaited.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stepByStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;from1To2:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nl"&gt;from10To11:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this runs, Drift sets &lt;code&gt;PRAGMA user_version&lt;/code&gt; to 11. The schema 12 columns are still in the file, but nothing reads them.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Skip debug schema validation after a downgrade
&lt;/h3&gt;

&lt;p&gt;The next run in debug mode produced a different error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Schema does not match
reports:
 columns:
  additional:
   Contains the following unexpected entries: submitted_at
settings:
 columns:
  additional:
   Contains the following unexpected entries: dark_mode_enabled, beta_features_enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This comes from &lt;code&gt;VerifySelf(db).validateDatabaseSchema()&lt;/code&gt; (from &lt;code&gt;drift_dev&lt;/code&gt;), which compares the real tables with the current schema. That check is usually wrapped in &lt;code&gt;kDebugMode&lt;/code&gt;, so &lt;strong&gt;it doesn't affect release builds&lt;/strong&gt;, but it will get in your way when testing locally. Skip it only when the database was downgraded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;beforeOpen:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;customStatement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'PRAGMA foreign_keys = ON'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// After a downgrade the tables still have the newer schema's columns, so&lt;/span&gt;
  &lt;span class="c1"&gt;// the debug-only schema validation would fail. Skip it in that case;&lt;/span&gt;
  &lt;span class="c1"&gt;// fresh installs and upgrades are still validated in debug builds.&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;isDowngrade&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionBefore&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionBefore&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionNow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isDowngrade&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;validateDatabaseSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Make the newer migration safe to run twice
&lt;/h3&gt;

&lt;p&gt;When a user goes &lt;strong&gt;12 → 11 → 12&lt;/strong&gt;, &lt;code&gt;from11To12&lt;/code&gt; runs &lt;strong&gt;a second time&lt;/strong&gt; against a database that already has those columns. A plain &lt;code&gt;addColumn&lt;/code&gt; would fail with &lt;code&gt;duplicate column name&lt;/code&gt;. Guard it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_columnExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;customSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'PRAGMA table_info(&lt;/span&gt;&lt;span class="si"&gt;$table&lt;/span&gt;&lt;span class="s"&gt;)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;any&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nl"&gt;from11To12:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_columnExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'settings'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'dark_mode_enabled'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settingsTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settingsTable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;darkModeEnabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...same for the other columns&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ideally the migration in the &lt;strong&gt;new release&lt;/strong&gt; is written this way from the start. It costs very little and makes a rollback much safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bug that's easy to introduce while making the fix
&lt;/h2&gt;

&lt;p&gt;My first version of the wrapper looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;onUpgrade:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;stepByStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;          &lt;span class="c1"&gt;// ❌ no await, and never called&lt;/span&gt;
    &lt;span class="nl"&gt;from1To2:&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compiles, and the downgrade test passes because it returns before reaching that line. But &lt;code&gt;stepByStep(...)&lt;/code&gt; only &lt;strong&gt;builds&lt;/strong&gt; the migration function. It's never called, so &lt;strong&gt;every normal upgrade does nothing&lt;/strong&gt;. Drift still writes the new &lt;code&gt;user_version&lt;/code&gt;, leaving users on older schemas with missing columns.&lt;/p&gt;

&lt;p&gt;The correct form is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;stepByStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; after touching &lt;code&gt;onUpgrade&lt;/code&gt;, always run your migration tests for the &lt;em&gt;upgrade&lt;/em&gt; path, not just the scenario you were fixing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flutter &lt;span class="nb"&gt;test test&lt;/span&gt;/drift/migration_test.dart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing locally without the release keystore
&lt;/h2&gt;

&lt;p&gt;Release signing lived in CI, so &lt;code&gt;flutter run --release&lt;/code&gt; wasn't possible locally. Two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Profile mode.&lt;/strong&gt; It isn't &lt;code&gt;kDebugMode&lt;/code&gt;, so it behaves like release for this test:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  flutter run &lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nt"&gt;--flavor&lt;/span&gt; production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Android complains about signing, temporarily point the build type at the debug signing config. &lt;strong&gt;Don't commit that change.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debug mode&lt;/strong&gt; with the validation guard from step 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Checking that the downgrade happened
&lt;/h3&gt;

&lt;p&gt;Log the versions in &lt;code&gt;beforeOpen&lt;/code&gt;. Use &lt;code&gt;print&lt;/code&gt;, because &lt;code&gt;kDebugMode&lt;/code&gt;-guarded logging doesn't appear in profile mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'[DB] versionBefore=&lt;/span&gt;&lt;span class="si"&gt;${details.versionBefore}&lt;/span&gt;&lt;span class="s"&gt; versionNow=&lt;/span&gt;&lt;span class="si"&gt;${details.versionNow}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;first launch of the rollback: &lt;code&gt;versionBefore=12 versionNow=11&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;next launch: &lt;code&gt;versionBefore=11 versionNow=11&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or check the file directly on Android:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb exec-out run-as com.example.myapp &lt;span class="nb"&gt;cat &lt;/span&gt;app_flutter/app.db &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; app.db
sqlite3 app.db &lt;span class="s2"&gt;"PRAGMA user_version;"&lt;/span&gt;            &lt;span class="c"&gt;# expect 11&lt;/span&gt;
sqlite3 app.db &lt;span class="s2"&gt;"PRAGMA table_info(settings);"&lt;/span&gt;    &lt;span class="c"&gt;# schema 12 columns still there, unused&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;getApplicationDocumentsDirectory()&lt;/code&gt; maps to &lt;code&gt;app_flutter/&lt;/code&gt; on Android. If the app is still running, also pull &lt;code&gt;app.db-wal&lt;/code&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The git workflow: two versions side by side with &lt;code&gt;git worktree&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;To test the round trip you need builds from &lt;strong&gt;two branches&lt;/strong&gt;. Your working branch probably has uncommitted local files (environment config, FVM settings and so on), so switching back and forth is painful. &lt;code&gt;git worktree&lt;/code&gt; checks another branch out into a &lt;strong&gt;separate folder&lt;/strong&gt; that uses the same repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# from the repo root&lt;/span&gt;
git worktree list
git worktree add ../myapp-v12 feature/new-release
&lt;span class="nb"&gt;cd&lt;/span&gt; ../myapp-v12

&lt;span class="c"&gt;# confirm it's really the newer schema&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"schemaVersion =&amp;gt;"&lt;/span&gt; lib/data/database/database.dart   &lt;span class="c"&gt;# expect 12&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Untracked local files aren't in git, so copy them across:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; ../myapp/.fvmrc &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; ../myapp/lib/core/env.dart lib/core/      &lt;span class="c"&gt;# make sure both builds use the same backend environment&lt;/span&gt;
fvm &lt;span class="nb"&gt;install
&lt;/span&gt;fvm flutter pub get
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One rule: &lt;strong&gt;a branch can only be checked out in one worktree at a time.&lt;/strong&gt; If you get &lt;code&gt;fatal: '&amp;lt;branch&amp;gt;' is already checked out&lt;/code&gt;, &lt;code&gt;git worktree list&lt;/code&gt; shows where it is.&lt;/p&gt;

&lt;p&gt;When you're done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree remove ../myapp-v12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building the three QA builds
&lt;/h2&gt;

&lt;p&gt;QA needs to test three transitions on &lt;strong&gt;one device without uninstalling&lt;/strong&gt;, because uninstalling deletes the database and makes the test pointless.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;APK&lt;/th&gt;
&lt;th&gt;Branch&lt;/th&gt;
&lt;th&gt;Schema&lt;/th&gt;
&lt;th&gt;Build number&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;feature/new-release&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;The new release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;release/1.4.2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;151&lt;/td&gt;
&lt;td&gt;The rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;feature/new-release&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;152&lt;/td&gt;
&lt;td&gt;Re-upgrade after the rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There are two Android rules to work around:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Same signing key.&lt;/strong&gt; Android won't install an update signed with a different key (&lt;code&gt;INSTALL_FAILED_UPDATE_INCOMPATIBLE&lt;/code&gt;). Build all three on the &lt;strong&gt;same machine&lt;/strong&gt;, so they're all signed with &lt;code&gt;~/.android/debug.keystore&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;versionCode&lt;/code&gt; must not go down&lt;/strong&gt; when the user taps an APK to install it. That's why &lt;strong&gt;C&lt;/strong&gt; exists: the same code as A, with a higher build number, so QA can install it over B. Without C, QA needs &lt;code&gt;adb install -r -d A.apk&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Commands
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/Desktop/QA-ROLLBACK
&lt;span class="nv"&gt;FLAVOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production
&lt;span class="nv"&gt;APK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;build/app/outputs/flutter-apk/app-&lt;span class="nv"&gt;$FLAVOR&lt;/span&gt;&lt;span class="nt"&gt;-debug&lt;/span&gt;.apk
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# B: the rollback (main folder, release/1.4.2)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/dev/myapp
fvm flutter clean &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; fvm flutter pub get
fvm flutter build apk &lt;span class="nt"&gt;--debug&lt;/span&gt; &lt;span class="nt"&gt;--flavor&lt;/span&gt; &lt;span class="nv"&gt;$FLAVOR&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;/B_rollback_v1.4.2-151_db11_debug.apk"&lt;/span&gt;

&lt;span class="c"&gt;# A: the new release (worktree, feature/new-release)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/dev/myapp-v12
fvm flutter pub get
fvm flutter build apk &lt;span class="nt"&gt;--debug&lt;/span&gt; &lt;span class="nt"&gt;--flavor&lt;/span&gt; &lt;span class="nv"&gt;$FLAVOR&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;/A_new-release_v1.5.0-150_db12_debug.apk"&lt;/span&gt;

&lt;span class="c"&gt;# C: re-upgrade (same worktree, higher build number)&lt;/span&gt;
fvm flutter build apk &lt;span class="nt"&gt;--debug&lt;/span&gt; &lt;span class="nt"&gt;--flavor&lt;/span&gt; &lt;span class="nv"&gt;$FLAVOR&lt;/span&gt; &lt;span class="nt"&gt;--build-number&lt;/span&gt; 152
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;/C_reupgrade_v1.5.0-152_db12_debug.apk"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Putting the order, version and schema in the file names makes it hard for QA to install them in the wrong order.&lt;/p&gt;

&lt;p&gt;Debug APKs include every CPU architecture and can be 150–250 MB. If your ticket system rejects them, build for physical phones only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fvm flutter build apk &lt;span class="nt"&gt;--debug&lt;/span&gt; &lt;span class="nt"&gt;--flavor&lt;/span&gt; &lt;span class="nv"&gt;$FLAVOR&lt;/span&gt; &lt;span class="nt"&gt;--target-platform&lt;/span&gt; android-arm64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The QA test plan
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Install order: A → B → C. Never uninstall in between.&lt;/strong&gt; If a store build is already on the device, uninstall it &lt;em&gt;once&lt;/em&gt; before A, because it's signed with a different key.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rollback (A → B).&lt;/strong&gt; Install A, log in, go offline, create unsynced data, and change a setting. Then install B over it.

&lt;ul&gt;
&lt;li&gt;Expected: the app opens, the user is still logged in, and all data is still there.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the app after the rollback.&lt;/strong&gt; On B, create new records in the tables that still have the newer columns, and change a setting. Then go back online.

&lt;ul&gt;
&lt;li&gt;Expected: saving works and everything syncs.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-upgrade (B → C).&lt;/strong&gt; Install C over B.

&lt;ul&gt;
&lt;li&gt;Expected: no migration error (the guarded &lt;code&gt;from11To12&lt;/code&gt; skips the existing columns), the data is intact, and the new features work.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regression: production → B.&lt;/strong&gt; Install the current store build, then B over it.

&lt;ul&gt;
&lt;li&gt;Expected: an 11 → 11 no-op with the data intact.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fresh install of B.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Expected: &lt;code&gt;onCreate&lt;/code&gt; runs and the default settings are seeded.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Shipping it
&lt;/h2&gt;

&lt;p&gt;Commit &lt;strong&gt;only&lt;/strong&gt; the database change. Generated plugin registrants, rebuilt web workers and local config files can stay out of the commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add lib/data/database/database.dart
git status &lt;span class="nt"&gt;--short&lt;/span&gt;     &lt;span class="c"&gt;# only this file should be staged&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Allow safe DB downgrade from schema 12 to 11 for rollback build"&lt;/span&gt;
git push origin release/1.4.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch for branches whose names differ only in case (&lt;code&gt;release/1.4.2&lt;/code&gt; vs &lt;code&gt;Release/1.4.2&lt;/code&gt;). Check what your branch tracks before you push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rev-parse &lt;span class="nt"&gt;--abbrev-ref&lt;/span&gt; @&lt;span class="o"&gt;{&lt;/span&gt;u&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When CI builds the store version of the rollback, its &lt;strong&gt;build number must be higher than the new release's&lt;/strong&gt;, or the stores won't offer it as an update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drift sends downgrades through &lt;code&gt;onUpgrade&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;beforeOpen&lt;/code&gt; runs afterwards, so it's too late to handle a downgrade there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;stepByStep&lt;/code&gt; throws on a downgrade.&lt;/strong&gt; Wrap it and check &lt;code&gt;from &amp;gt; to&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the newer schema is additive, the safest downgrade is a no-op.&lt;/strong&gt; Keep the data. Only drop or rebuild tables when you really have to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the newer migrations idempotent&lt;/strong&gt; (&lt;code&gt;_columnExists&lt;/code&gt; checks) so 12 → 11 → 12 works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip debug-only schema validation&lt;/strong&gt; after a downgrade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;stepByStep(...)&lt;/code&gt; returns a function.&lt;/strong&gt; Call it and &lt;code&gt;await&lt;/code&gt; it, and run your upgrade tests after changing &lt;code&gt;onUpgrade&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prepare the rollback before you need it.&lt;/strong&gt; Test the downgrade path before release day, not during an incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git worktree&lt;/code&gt; + same-key debug builds + build-number control&lt;/strong&gt; lets QA test the whole upgrade → rollback → re-upgrade cycle on one device.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your next release adds anything other than nullable columns, plan the downgrade path &lt;strong&gt;before&lt;/strong&gt; you ship it.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>mobile</category>
      <category>drift</category>
    </item>
    <item>
      <title>Building a Multi-Tenant Production-Grade Wallet Pass System for Google &amp; Apple: A Complete Architectural &amp; Implementation Guide</title>
      <dc:creator>George Ikwegbu Chinedu</dc:creator>
      <pubDate>Thu, 22 Oct 2026 19:47:05 +0000</pubDate>
      <link>https://dev.to/gikwegbu/building-a-multi-tenant-production-grade-wallet-pass-system-for-google-apple-a-complete-ncj</link>
      <guid>https://dev.to/gikwegbu/building-a-multi-tenant-production-grade-wallet-pass-system-for-google-apple-a-complete-ncj</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Your customers already carry everything on their phones — and their native digital wallet apps (Apple Wallet on iOS and Google Wallet on Android) are accessed daily. Building a production-grade system that issues real digital passes transforms customer engagement: digital loyalty cards, tickets, and membership cards that users add with a single tap, receive live dynamic updates on their lock screens, and scan at physical locations to redeem rewards or verify admission.&lt;/p&gt;

&lt;p&gt;This guide provides an end-to-end, production-tested blueprint for architecting and implementing a dual-platform wallet pass engine. The concepts, cryptographic signing pipelines, API specifications, and database models presented here are deliberately stack-agnostic — applicable whether your backend is built with TypeScript/Node.js, Go, Python, Java, or Rust, and whether your database is PostgreSQL, MySQL, or a document store.&lt;/p&gt;

&lt;p&gt;By the end of this guide, you will master:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Google Wallet Architecture&lt;/strong&gt;: From Google Cloud service account provisioning and Google Pay Console issuer setup to signing JWT passes and handling class inheritance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple Wallet &amp;amp; PassKit Architecture&lt;/strong&gt;: Managing certificate authorities, &lt;code&gt;.p12&lt;/code&gt; to &lt;code&gt;.pem&lt;/code&gt; extraction, WWDR G4 chains, APNs push channels, and strict PassKit validation rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local Development &amp;amp; Mobile Testing via HTTPS Tunneling (ngrok)&lt;/strong&gt;: How to satisfy Apple’s non-negotiable HTTPS requirement and Google’s JWT origin validation when testing on physical smartphones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass Design &amp;amp; Cryptographic QR Redemptions&lt;/strong&gt;: Structuring fields, image bundles, and tamper-proof, short-lived QR codes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Tenant System Design&lt;/strong&gt;: How a single platform issuer can host hundreds of individual brands with their own custom colors, logos, and points systems without requiring separate Apple or Google developer accounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customer Identity &amp;amp; Junction Data Modeling&lt;/strong&gt;: Handling customers who belong to multiple businesses simultaneously without data leaks or duplicate pass collisions.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;📂 &lt;strong&gt;Reference Implementation &amp;amp; Open Source Repository&lt;/strong&gt;:&lt;br&gt;
A complete, production-ready reference implementation of this architecture — featuring a NestJS backend, PostgreSQL/Prisma persistence, PassKit signing pipeline, and a Vue 3 Tailwind admin dashboard — is available on GitHub:&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://github.com/gikwegbu/add_to_google_apple_wallet" rel="noopener noreferrer"&gt;https://github.com/gikwegbu/add_to_google_apple_wallet&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Contributions, issue reports, and pull requests are warmly welcome!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 1 — Google Wallet: From Zero to Issuer
&lt;/h2&gt;

&lt;p&gt;Google Wallet passes are powered by the &lt;strong&gt;Google Wallet REST API&lt;/strong&gt; and authenticated using &lt;strong&gt;JSON Web Tokens (JWTs)&lt;/strong&gt; signed with RS256 using an asymmetric Google Service Account key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────────────────┐
│                        GOOGLE WALLET ECOSYSTEM                         │
├────────────────────────────────┬───────────────────────────────────────┤
│      GOOGLE CLOUD CONSOLE      │      GOOGLE PAY &amp;amp; WALLET CONSOLE       │
│  - Cloud Project               │  - Issuer Account (Issuer ID)         │
│  - Google Wallet API Enabled   │  - Service Account Authorization      │
│  - Service Account (RSA Key)   │  - Pass Class Definitions             │
└────────────────────────────────┴───────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1.1 Create a Google Cloud Project
&lt;/h3&gt;

&lt;p&gt;Every Google Wallet integration lives under a Google Cloud Project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to the &lt;strong&gt;&lt;a href="https://console.cloud.google.com/" rel="noopener noreferrer"&gt;Google Cloud Console&lt;/a&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Create a new project (e.g., &lt;code&gt;wallet-pass-platform&lt;/code&gt;). Choose a name reflecting your platform rather than a single client brand.&lt;/li&gt;
&lt;li&gt;Ensure billing is enabled for the project.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1.2 Enable the Google Wallet API
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In your Cloud Project, open &lt;strong&gt;APIs &amp;amp; Services → Library&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Search for &lt;strong&gt;Google Wallet API&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Enable&lt;/strong&gt;. This activates the endpoints under &lt;code&gt;walletobjects.googleapis.com&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1.3 Provision a Dedicated Service Account
&lt;/h3&gt;

&lt;p&gt;Your backend server authenticates with Google using an automated Service Account identity:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;IAM &amp;amp; Admin → Service Accounts&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create Service Account&lt;/strong&gt;, naming it descriptively (e.g., &lt;code&gt;wallet-issuer-sa&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Click into the newly created service account and navigate to the &lt;strong&gt;Keys&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Add Key → Create new key → JSON&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Download the JSON credential file and store it securely on your server (e.g., &lt;code&gt;core/service-account.json&lt;/code&gt;). &lt;strong&gt;Never commit this file or its private key to version control.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The downloaded file contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;client_email&lt;/code&gt;: The service account email (e.g., &lt;code&gt;wallet-issuer-sa@project-id.iam.gserviceaccount.com&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;private_key&lt;/code&gt;: The RSA 2048-bit private key (PEM format with &lt;code&gt;\n&lt;/code&gt; characters).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1.4 Register in the Google Pay &amp;amp; Wallet Console
&lt;/h3&gt;

&lt;p&gt;Having the Cloud Project API enabled is not enough. You must register as an authorized pass issuer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the &lt;strong&gt;&lt;a href="https://pay.google.com/business/console/" rel="noopener noreferrer"&gt;Google Pay &amp;amp; Wallet Console&lt;/a&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Complete the business registration profile (Legal Business Name, Website, Point of Contact).&lt;/li&gt;
&lt;li&gt;Once approved, note your &lt;strong&gt;Issuer ID&lt;/strong&gt; (a numeric string such as &lt;code&gt;3388000000023066256&lt;/code&gt;). This permanent identifier namespaces every class and object you ever produce.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1.5 Authorize the Service Account in the Wallet Console
&lt;/h3&gt;

&lt;p&gt;The Cloud Console and the Pay &amp;amp; Wallet Console are independent security domains. You must bridge them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;strong&gt;Google Pay &amp;amp; Wallet Console&lt;/strong&gt;, navigate to the &lt;strong&gt;Users&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Invite a user&lt;/strong&gt; (or &lt;strong&gt;Add User&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Paste the full &lt;code&gt;client_email&lt;/code&gt; from your service account JSON file.&lt;/li&gt;
&lt;li&gt;Set the permission level to &lt;strong&gt;Developer&lt;/strong&gt; or &lt;strong&gt;Admin&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Save. &lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Common Trap&lt;/strong&gt;: If you omit this step, any API call or pass creation will fail with &lt;code&gt;401 Unauthorized: Request had invalid authentication credentials&lt;/code&gt; or &lt;code&gt;403 Forbidden: Not an issuer&lt;/code&gt;, even if the Service Account is Project Owner in Google Cloud.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1.6 Pass Classes vs. Pass Objects
&lt;/h3&gt;

&lt;p&gt;Google Wallet separates templates from individual user passes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pass Class (Template)&lt;/strong&gt;: Defines the global layout, program name, brand logos, primary colors, review status, and callback URLs. Created &lt;strong&gt;once per brand/program&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass Object (Instance)&lt;/strong&gt;: Represents a single user's pass. References the &lt;code&gt;classId&lt;/code&gt; and holds specific values (User's Name, Current Points Balance, Unique Barcode, Account ID).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google supports several class types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;genericClass&lt;/code&gt; / &lt;code&gt;genericObject&lt;/code&gt;: Best for flexible reward cards, club memberships, vouchers, and event tickets. Highly customizable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;loyaltyClass&lt;/code&gt; / &lt;code&gt;loyaltyObject&lt;/code&gt;: Specifically tailored for retail points programs with structured tier labels.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;eventTicketClass&lt;/code&gt; / &lt;code&gt;eventTicketObject&lt;/code&gt;: Tailored for seated venue events with seat/gate numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Class IDs follow the format: &lt;code&gt;&amp;lt;Issuer_ID&amp;gt;.&amp;lt;custom_class_suffix&amp;gt;&lt;/code&gt; (e.g., &lt;code&gt;3388000000023066256.summer_rewards_class&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  1.7 Generating the Save Link: The "Skinny" JWT Flow
&lt;/h3&gt;

&lt;p&gt;You do not need to make an upfront REST API call to pre-create every object before a user saves it. Google provides an atomic, high-performance issuance pattern known as the &lt;strong&gt;Skinny JWT&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; ┌─────────────────┐       GET /google/pass/:id       ┌─────────────────┐
 │   User Browser  │ ───────────────────────────────&amp;gt; │  Your Backend   │
 │                 │ &amp;lt;─────────────────────────────── │  Signs JWT      │
 └─────────────────┘       302 Redirect to Save URL   └─────────────────┘
          │
          ▼
 ┌─────────────────┐
 │ Google Pay Save │ ──&amp;gt; Pass added to Google Wallet app instantly!
 └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  JWT Structure
&lt;/h4&gt;

&lt;p&gt;The backend constructs an RS256-signed JWT containing the pass object and returns a 302 redirect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JWT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Header&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JWT"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JWT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Claims&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Payload&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wallet-issuer-sa@project-id.iam.gserviceaccount.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"google"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"savetowallet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1729600000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"origins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:3000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://your-domain.ngrok-free.dev"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"genericObjects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3388000000023066256.user-uuid-1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"classId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3388000000023066256.summer_rewards_class"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACTIVE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"cardTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"defaultValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en-US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Reisty Rewards"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"header"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"defaultValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en-US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"George Ikwegbu"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"textModulesData"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"points"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"header"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POINTS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"120"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"barcode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"QR_CODE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;signed_qr_payload&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"alternateText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Scan at terminal"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Critical Requirement&lt;/strong&gt;: The &lt;code&gt;origins&lt;/code&gt; claim is required by Google. It must match the domain or IP from which the user initiates the Save flow. For local and mobile testing, include your HTTPS tunnel domain (see Part 4).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The user is redirected to:&lt;br&gt;
&lt;code&gt;https://pay.google.com/gp/v/save/&amp;lt;signed_jwt&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When clicked on Android or desktop Chrome, Google verifies the cryptographic signature against your Service Account public key, provisions the object on their servers, and adds it to the user's Google Wallet immediately.&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 2 — Apple Wallet: Developer Setup, PassKit &amp;amp; Certificates
&lt;/h2&gt;

&lt;p&gt;Apple Wallet passes (&lt;code&gt;.pkpass&lt;/code&gt;) are signed ZIP archives verified by iOS using Public Key Cryptography Standards (PKCS#7). iOS strictly validates the signature against Apple's Worldwide Developer Relations (WWDR) Certificate Authority.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                      APPLE PASSKIT SIGNING PIPELINE

  pass.json + icon.png + logo.png
               │
               ▼
  Generate manifest.json (SHA-1 checksum of every asset)
               │
               ▼
  Sign manifest.json using Pass Certificate + Private Key + WWDR G4
  (Generates detached PKCS#7 signature)
               │
               ▼
  Compress into ZIP archive named &amp;lt;user_name&amp;gt;_pass.pkpass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.1 Apple Developer Setup
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Enroll in the &lt;strong&gt;&lt;a href="https://developer.apple.com/programs/" rel="noopener noreferrer"&gt;Apple Developer Program&lt;/a&gt;&lt;/strong&gt; (annual $99 membership).&lt;/li&gt;
&lt;li&gt;Locate your &lt;strong&gt;Team ID&lt;/strong&gt; under &lt;strong&gt;Account → Membership Details&lt;/strong&gt; (a 10-character alphanumeric string like &lt;code&gt;A1B2C3D4E5&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2.2 Register a Pass Type Identifier
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Certificates, Identifiers &amp;amp; Profiles → Identifiers&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+&lt;/strong&gt;, select &lt;strong&gt;Pass Type IDs&lt;/strong&gt;, and click &lt;strong&gt;Continue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Provide a Description (e.g. &lt;code&gt;Platform Loyalty Passes&lt;/code&gt;) and an Identifier formatted as &lt;code&gt;pass.&amp;lt;organization&amp;gt;.&amp;lt;product&amp;gt;&lt;/code&gt; (e.g. &lt;code&gt;pass.com.yourcompany.wallet&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Register the identifier.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2.3 Create &amp;amp; Export Signing Certificates
&lt;/h3&gt;

&lt;p&gt;Passes must be signed by a certificate issued specifically for your Pass Type ID.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In &lt;strong&gt;Keychain Access&lt;/strong&gt; on macOS, select &lt;strong&gt;Certificate Assistant → Request a Certificate from a Certificate Authority&lt;/strong&gt;.

&lt;ul&gt;
&lt;li&gt;Enter your email, select &lt;strong&gt;Saved to disk&lt;/strong&gt;, and leave CA Email blank. This creates a &lt;code&gt;CertificateSigningRequest.certSigningRequest&lt;/code&gt; (CSR).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In the Apple Developer portal under &lt;strong&gt;Identifiers → Pass Type IDs&lt;/strong&gt;, select your ID and click &lt;strong&gt;Create Certificate&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Upload your CSR. Download the resulting &lt;code&gt;pass.cer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Double-click &lt;code&gt;pass.cer&lt;/code&gt; to install it into Keychain Access.
&amp;gt; 💡 &lt;strong&gt;Keychain Tip&lt;/strong&gt;: Ensure the certificate is installed in the &lt;strong&gt;login&lt;/strong&gt; keychain, NOT the System keychain; otherwise, macOS will grey out the &lt;code&gt;.p12&lt;/code&gt; export option.&lt;/li&gt;
&lt;li&gt;In Keychain Access, right-click the Pass Type ID certificate and choose &lt;strong&gt;Export "Pass Type ID: ..."&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Export as &lt;code&gt;.p12&lt;/code&gt; format (e.g. &lt;code&gt;pass.p12&lt;/code&gt;). Enter an export password — this is your &lt;code&gt;APPLE_CERT_PASSPHRASE&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2.4 Convert to Server-Ready PEM Files
&lt;/h3&gt;

&lt;p&gt;Convert the &lt;code&gt;.p12&lt;/code&gt; file and Apple’s root authority certificate into unencrypted PEM files for your backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Extract the public pass certificate&lt;/span&gt;
openssl pkcs12 &lt;span class="nt"&gt;-in&lt;/span&gt; pass.p12 &lt;span class="nt"&gt;-clcerts&lt;/span&gt; &lt;span class="nt"&gt;-nokeys&lt;/span&gt; &lt;span class="nt"&gt;-out&lt;/span&gt; passcertificate.pem

&lt;span class="c"&gt;# 2. Extract the unencrypted private key&lt;/span&gt;
openssl pkcs12 &lt;span class="nt"&gt;-in&lt;/span&gt; pass.p12 &lt;span class="nt"&gt;-nocerts&lt;/span&gt; &lt;span class="nt"&gt;-nodes&lt;/span&gt; &lt;span class="nt"&gt;-out&lt;/span&gt; passkey.pem

&lt;span class="c"&gt;# 3. Download Apple Worldwide Developer Relations (WWDR) G4 Certificate&lt;/span&gt;
curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://www.apple.com/certificateauthority/AppleWWDRCAG4.cer

&lt;span class="c"&gt;# 4. Convert WWDR from DER to PEM&lt;/span&gt;
openssl x509 &lt;span class="nt"&gt;-inform&lt;/span&gt; DER &lt;span class="nt"&gt;-in&lt;/span&gt; AppleWWDRCAG4.cer &lt;span class="nt"&gt;-out&lt;/span&gt; wwdr.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store &lt;code&gt;passcertificate.pem&lt;/code&gt;, &lt;code&gt;passkey.pem&lt;/code&gt;, and &lt;code&gt;wwdr.pem&lt;/code&gt; in &lt;code&gt;core/apple/certs/&lt;/code&gt;. Add this folder to &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.5 The .pkpass Package Anatomy &amp;amp; Mandatory Assets
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;.pkpass&lt;/code&gt; bundle is a standard ZIP file with an exact file hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;George_Ikwegbu_pass.pkpass
├── pass.json          ← Data structure, text fields, colors, serial number
├── icon.png           ← MANDATORY: 29x29px (shown in iOS notifications &amp;amp; pass list)
├── icon@2x.png        ← MANDATORY: 58x58px
├── icon@3x.png        ← MANDATORY: 87x87px
├── logo.png           ← MANDATORY: 160x50px (pass header face)
├── logo@2x.png        ← MANDATORY: 320x100px
├── manifest.json      ← SHA-1 checksum map of every file above
└── signature          ← Detached PKCS#7 signature of manifest.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Strict Asset Rules&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apple PassKit &lt;strong&gt;will fail&lt;/strong&gt; with &lt;code&gt;SyntaxError: Missing required image icon.png&lt;/code&gt; if &lt;code&gt;icon.png&lt;/code&gt; is absent from the bundle template.&lt;/li&gt;
&lt;li&gt;Never include &lt;code&gt;manifest.json&lt;/code&gt; or &lt;code&gt;signature&lt;/code&gt; inside &lt;code&gt;manifest.json&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2.6 Apple PassKit Validation Rules &amp;amp; Gotchas
&lt;/h3&gt;

&lt;p&gt;When building with libraries such as &lt;code&gt;@walletpass/pass-js&lt;/code&gt; (Node), &lt;code&gt;passkit-generator&lt;/code&gt; (Python), or &lt;code&gt;passbook&lt;/code&gt; (Ruby), you must satisfy two critical Apple requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;authenticationToken&lt;/code&gt; Length Requirement&lt;/strong&gt;:
Apple PassKit mandates that if a pass defines a &lt;code&gt;webServiceURL&lt;/code&gt;, the &lt;code&gt;authenticationToken&lt;/code&gt; &lt;strong&gt;must be at least 16 characters long&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;   &lt;span class="c1"&gt;// ❌ FAILS: Throws TypeError: authenticationToken must be 16 characters or longer&lt;/span&gt;
   &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticationToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// ✅ CORRECT: Increase it&lt;/span&gt;
   &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticationToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

   &lt;span class="c1"&gt;// ✅ CORRECT: Cryptographically random 32-character hex token&lt;/span&gt;
   &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticationToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Apple Web Service Protocol Requires HTTPS&lt;/strong&gt;:
Apple devices strictly refuse to register passes or fetch background updates if &lt;code&gt;webServiceURL&lt;/code&gt; uses plain &lt;code&gt;http://&lt;/code&gt;. It &lt;strong&gt;must&lt;/strong&gt; be HTTPS with a valid TLS certificate.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Part 3 — Testing Live Passes Locally via HTTPS Tunneling (ngrok)
&lt;/h2&gt;

&lt;p&gt;Developing wallet passes locally presents a challenge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apple Wallet on iOS requires a public HTTPS &lt;code&gt;webServiceURL&lt;/code&gt; to register passes and receive background push updates.&lt;/li&gt;
&lt;li&gt;Google Wallet JWT tokens enforce an &lt;code&gt;origins&lt;/code&gt; check and require publicly resolvable image URLs.&lt;/li&gt;
&lt;li&gt;Testing on physical iPhones and Android devices requires your computer’s local dev server to be reachable from mobile devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is resolved using an HTTPS tunnel such as &lt;strong&gt;&lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt;&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; ┌─────────────────┐       HTTPS Requests       ┌──────────────┐      Port 3000      ┌──────────────┐
 │ Physical Phone  │ ─────────────────────────&amp;gt; │ ngrok Tunnel │ ──────────────────&amp;gt; │ Local Server │
 │ (iOS / Android) │ &amp;lt;───────────────────────── │ Public HTTPS │ &amp;lt;────────────────── │ (Nest / Exp) │
 └─────────────────┘                            └──────────────┘                     └──────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.1 Starting the Tunnel
&lt;/h3&gt;

&lt;p&gt;Launch an HTTPS tunnel targeting your local backend server port (e.g. port 3000):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Standard dynamic tunnel&lt;/span&gt;
ngrok http 3000

&lt;span class="c"&gt;# Or using a persistent static domain (recommended)&lt;/span&gt;
ngrok http &lt;span class="nt"&gt;--url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://your-domain.ngrok-free.dev 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ngrok will output a secure HTTPS endpoint:&lt;br&gt;
&lt;code&gt;https://your-domain.ngrok-free.dev -&amp;gt; http://localhost:3000&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3.2 Configuring the Backend
&lt;/h3&gt;

&lt;p&gt;Update your server configuration (e.g. &lt;code&gt;core/.env&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PORT=3000
API_BASE_URL=https://your-domain.ngrok-free.dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your pass generation logic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Google Wallet&lt;/strong&gt;: Add &lt;code&gt;process.env.API_BASE_URL&lt;/code&gt; to the JWT &lt;code&gt;origins&lt;/code&gt; array:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;   &lt;span class="nx"&gt;origins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Apple Wallet&lt;/strong&gt;: PassKit sets the registration URL dynamically:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webServiceURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;/api/apple`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webServiceURL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webServiceURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;webServiceURL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticationToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, clicking pass generation links on physical devices or in your web dashboard allows iOS and Android to connect directly to your local workstation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4 — Interactive Pass Endpoints &amp;amp; Frontend UX
&lt;/h2&gt;

&lt;p&gt;A clean user experience offers dedicated chips for both mobile ecosystems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────────────────┐
│ USER                POINTS     PASSES                        ACTIONS   │
├────────────────────────────────────────────────────────────────────────┤
│ James Groka         ° 80       [ ● GOOGLE ]   [ ● APPLE ]    [⎘] [🗑] [⋮] │
└────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.1 Server Pass Endpoints
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Google Pass Redirect Endpoint
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;GET /google/pass/:userId&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Looks up the user and active pass configuration in the database.&lt;/li&gt;
&lt;li&gt;Generates the signed Google JWT.&lt;/li&gt;
&lt;li&gt;Responds with &lt;code&gt;302 Found&lt;/code&gt; redirecting directly to &lt;code&gt;https://pay.google.com/gp/v/save/&amp;lt;jwt&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Clicking this link with &lt;code&gt;target="_blank"&lt;/code&gt; on desktop or mobile opens the native Google Wallet save dialogue.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Apple Pass Download Endpoint
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;GET /apple/pass/:userId&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Looks up the user and active pass template.&lt;/li&gt;
&lt;li&gt;Injects the user’s name, points, barcode, and unique serial number into &lt;code&gt;pass.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Signs and compresses the &lt;code&gt;.pkpass&lt;/code&gt; bundle in memory.&lt;/li&gt;
&lt;li&gt;Sanitizes the user's name for the download filename (e.g. &lt;code&gt;George_Ikwegbu_pass.pkpass&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Returns binary data with standard PassKit headers:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;   &lt;/span&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="s"&gt;   Content-Type: application/vnd.apple.pkpass&lt;/span&gt;
&lt;span class="s"&gt;   Content-Disposition: attachment; filename="George_Ikwegbu_pass.pkpass"&lt;/span&gt;
&lt;span class="s"&gt;   Cache-Control: no-store, no-cache, must-revalidate, max-age=0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.2 Frontend Chip Design &amp;amp; Interaction
&lt;/h3&gt;

&lt;p&gt;In your frontend dashboard (Vue, React, or standard HTML), implement two distinct pills matching platform conventions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Chip&lt;/strong&gt;: Light grey/white pill (&lt;code&gt;#F8FAFB&lt;/code&gt;), subtle border (&lt;code&gt;border-slate-200&lt;/code&gt;), rounded rectangle (&lt;code&gt;rounded-lg&lt;/code&gt;), uppercase bold label, and a blue indicator dot (&lt;code&gt;#1A73E8&lt;/code&gt;).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; 
    &lt;span class="na"&gt;:href=&lt;/span&gt;&lt;span class="s"&gt;"`${apiBaseUrl}/google/pass/${user.id}`"&lt;/span&gt; 
    &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"_blank"&lt;/span&gt; 
    &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"noopener noreferrer"&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"inline-flex items-center px-3 py-1.5 bg-[#F8FAFB] hover:bg-slate-100 border border-slate-200 rounded-lg text-[11px] font-bold text-slate-700 tracking-wider"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-2 h-2 rounded-full bg-[#1A73E8] mr-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    GOOGLE
  &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apple Chip&lt;/strong&gt;: Identical pill container with a dark slate/black indicator dot (&lt;code&gt;#0F172B&lt;/code&gt;).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; 
    &lt;span class="na"&gt;:href=&lt;/span&gt;&lt;span class="s"&gt;"`${apiBaseUrl}/apple/pass/${user.id}`"&lt;/span&gt; 
    &lt;span class="na"&gt;:download=&lt;/span&gt;&lt;span class="s"&gt;"`${user.fullname.replace(/\s+/g, '_')}_pass.pkpass`"&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"inline-flex items-center px-3 py-1.5 bg-[#F8FAFB] hover:bg-slate-100 border border-slate-200 rounded-lg text-[11px] font-bold text-slate-700 tracking-wider"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-2 h-2 rounded-full bg-[#0F172B] mr-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    APPLE
  &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When clicked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;Google chip&lt;/strong&gt; opens a new tab that immediately redirects into Google Wallet's cloud save prompt.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Apple chip&lt;/strong&gt; triggers a browser download of the &lt;code&gt;.pkpass&lt;/code&gt; bundle on desktop, or launches the Apple Wallet installation sheet on iOS Safari.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 5 — Designing Passes &amp;amp; Tamper-Proof QR Codes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 Barcodes &amp;amp; Cryptographic Redemptions
&lt;/h3&gt;

&lt;p&gt;A common security vulnerability in loyalty passes is encoding raw customer IDs in the QR code (e.g. &lt;code&gt;{"userId": "123"}&lt;/code&gt;). A user can screenshot their QR code or copy another customer’s ID and redeem unauthorized points.&lt;/p&gt;

&lt;h4&gt;
  
  
  Secure Tokenized QR Architecture:
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Server Signs a Short-Lived JWT for the QR&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-uuid-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"tenantId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tenant-uuid-456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1729600000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1729603600&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scanner Validates Authenticity&lt;/strong&gt;:
When the merchant scans the QR code at the terminal:

&lt;ul&gt;
&lt;li&gt;Validates the cryptographic signature against the platform secret.&lt;/li&gt;
&lt;li&gt;Verifies &lt;code&gt;exp&lt;/code&gt; has not lapsed.&lt;/li&gt;
&lt;li&gt;Atomically decrements points in the database.&lt;/li&gt;
&lt;li&gt;Triggers background pass balance updates to both Apple and Google.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  5.2 Dynamic Pass Updates
&lt;/h3&gt;

&lt;p&gt;When a customer’s balance updates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Wallet&lt;/strong&gt;: Execute a &lt;code&gt;PATCH&lt;/code&gt; request to &lt;code&gt;https://walletobjects.googleapis.com/walletobjects/v1/genericObject/&amp;lt;objectId&amp;gt;&lt;/code&gt; updating the text modules. Google pushes an automated lock-screen update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple Wallet&lt;/strong&gt;: 

&lt;ol&gt;
&lt;li&gt;Update the database record.&lt;/li&gt;
&lt;li&gt;Send an APNs background push notification containing an empty JSON body &lt;code&gt;{}&lt;/code&gt; to all registered device tokens for that pass serial number.&lt;/li&gt;
&lt;li&gt;iOS wakes up silently, calls your server's &lt;code&gt;GET /v1/passes/:passTypeIdentifier/:serialNumber&lt;/code&gt;, and replaces the &lt;code&gt;.pkpass&lt;/code&gt; bundle on device.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 6 — Multi-Tenant Architecture: One Platform, Hundreds of Brands
&lt;/h2&gt;

&lt;p&gt;In a multi-tenant platform (SaaS for restaurants, fitness studios, or ticketing agencies), each business wants its own logo, colors, and rewards structure.&lt;/p&gt;

&lt;p&gt;You do &lt;strong&gt;not&lt;/strong&gt; need separate Apple Developer accounts or Google Cloud projects for each tenant. A single platform issuer can host hundreds of branded passes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Platform (Issuer)
│   Google Issuer ID: 3388000000023066256
│   Apple Pass Type ID: pass.com.yourplatform.wallet
│
├── Tenant A: Restaurant Rosso
│     Google Class: 3388000000023066256.rosso_loyalty
│     Apple Template: templates/rosso/pass.json + red branding
│     Customers receive Rosso-branded passes
│
└── Tenant B: Café Verde
      Google Class: 3388000000023066256.verde_loyalty
      Apple Template: templates/verde/pass.json + green branding
      Customers receive Verde-branded passes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.1 Tenant Class ID Convention
&lt;/h3&gt;

&lt;p&gt;Adopt deterministic namespacing tied to your internal database UUIDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Google Class ID:    &amp;lt;Issuer_ID&amp;gt;.&amp;lt;tenant_uuid&amp;gt;_loyalty
Apple Template:     storage/tenants/&amp;lt;tenant_uuid&amp;gt;/pass.json
Apple Icons/Logos:  storage/tenants/&amp;lt;tenant_uuid&amp;gt;/logo.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a business updates its logo or primary color in your admin portal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google&lt;/strong&gt;: Call &lt;code&gt;PATCH /walletobjects/v1/genericClass/&amp;lt;classId&amp;gt;&lt;/code&gt; with the new asset URLs. Google propagates the change to all existing customer passes automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple&lt;/strong&gt;: Update the tenant's asset directory and trigger APNs push notifications to update devices.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 7 — One Customer, Many Tenants: The Junction Model
&lt;/h2&gt;

&lt;p&gt;Customers frequently visit multiple businesses on the same platform. A user might hold a loyalty pass for &lt;em&gt;Restaurant Rosso&lt;/em&gt; and another for &lt;em&gt;Café Verde&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.1 Relational Data Model
&lt;/h3&gt;

&lt;p&gt;Decouple the global user profile from their tenant-specific membership using a &lt;strong&gt;Junction Record&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Account: george@example.com (Global ID: user-101)
│
├── Tenant Membership (Rosso)
│     points: 80
│     googleObjectId: 3388000000023066256.rosso_user_101
│     appleSerialNumber: rosso_user_101
│
└── Tenant Membership (Verde)
      points: 30
      googleObjectId: 3388000000023066256.verde_user_101
      appleSerialNumber: verde_user_101
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Relational Schema Representation&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fullname&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;tenant_memberships&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;tenants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;points&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;google_pass_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;apple_pass_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7.2 Idempotent Pass Issuance
&lt;/h3&gt;

&lt;p&gt;To prevent duplicate passes when a user clicks "Add to Wallet" multiple times, employ an upsert pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /passes/issue
Body: { userId, tenantId, platform }

1. Upsert tenant_memberships WHERE user_id = userId AND tenant_id = tenantId.
2. If membership already possesses a valid pass ID:
   -&amp;gt; Return existing Google Save URL or stream existing Apple .pkpass.
3. If new membership:
   -&amp;gt; Mint unique pass IDs: `${tenant_id}_${user_id}`
   -&amp;gt; Persist pass ID in membership record.
   -&amp;gt; Return generated Save URL / .pkpass.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusion &amp;amp; Production Checklist
&lt;/h2&gt;

&lt;p&gt;Building a unified wallet pass system bridges digital interaction with real-world physical verification. By structuring your backend around standards-compliant JWTs for Google Wallet, strictly validated PassKit packages for Apple Wallet, and an HTTPS tunnel for live testing, you create a robust, production-ready foundation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production Pre-Flight Checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Google Service Account&lt;/strong&gt;: Authorized as Developer/Admin in the Google Pay &amp;amp; Wallet Console.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;JWT Origins&lt;/strong&gt;: Configured with your live production HTTPS domain.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Apple Certificates&lt;/strong&gt;: Extracted into &lt;code&gt;passcertificate.pem&lt;/code&gt;, &lt;code&gt;passkey.pem&lt;/code&gt;, and &lt;code&gt;wwdr.pem&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;PassKit Asset Validation&lt;/strong&gt;: Template contains &lt;code&gt;icon.png&lt;/code&gt; (29x29) and &lt;code&gt;logo.png&lt;/code&gt; (160x50) in all pixel densities.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;PassKit Token&lt;/strong&gt;: &lt;code&gt;authenticationToken&lt;/code&gt; is verified to be $\ge 16$ characters.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Security &amp;amp; &lt;code&gt;.gitignore&lt;/code&gt;&lt;/strong&gt;: All &lt;code&gt;.pem&lt;/code&gt;, &lt;code&gt;.p12&lt;/code&gt;, service account JSON files, and environment files (&lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.env.*&lt;/code&gt;) are untracked and excluded from version control, leaving only &lt;code&gt;.env.example&lt;/code&gt; and &lt;code&gt;pass.json.example&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Local Testing&lt;/strong&gt;: Verified live pass installation on physical iOS and Android devices using ngrok HTTPS tunneling.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Open Source Repository &amp;amp; Community Contributions
&lt;/h2&gt;

&lt;p&gt;The complete codebase backing this guide — including the backend pass generation controllers, Apple PassKit signing pipeline, and the Vue 3 Tailwind admin dashboard — is open-source on GitHub:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/gikwegbu/add_to_google_apple_wallet" rel="noopener noreferrer"&gt;https://github.com/gikwegbu/add_to_google_apple_wallet&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  We Welcome Contributions!
&lt;/h3&gt;

&lt;p&gt;This project is open-source and actively evolving. If you would like to contribute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fork the repository&lt;/strong&gt; on GitHub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Submit Pull Requests&lt;/strong&gt; for new features, bug fixes, automated pass push notifications, or documentation improvements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open Issues&lt;/strong&gt; to report bugs or suggest enhancements.&lt;/li&gt;
&lt;li&gt;If this project helped you or your team, consider giving it a ⭐ on GitHub!&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>google</category>
      <category>wallet</category>
      <category>multitenant</category>
      <category>software</category>
    </item>
    <item>
      <title>Shipping a Safe Rollback: Downgrading a Drift (SQLite) Database in Flutter Without Losing User Data</title>
      <dc:creator>George Ikwegbu Chinedu</dc:creator>
      <pubDate>Tue, 20 Oct 2026 20:09:26 +0000</pubDate>
      <link>https://dev.to/gikwegbu/shipping-a-safe-rollback-downgrading-a-drift-sqlite-database-in-flutter-without-losing-user-data-2d0p</link>
      <guid>https://dev.to/gikwegbu/shipping-a-safe-rollback-downgrading-a-drift-sqlite-database-in-flutter-without-losing-user-data-2d0p</guid>
      <description>&lt;p&gt;You're about to ship a release that bumps your local database schema. Being careful, you also prepare a &lt;strong&gt;rollback build&lt;/strong&gt;, the version currently in production, so that if the new release has problems you can push the old one straight back out.&lt;/p&gt;

&lt;p&gt;Then you test the rollback, and it crashes on startup.&lt;/p&gt;

&lt;p&gt;This post covers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Why Drift refuses to "upgrade" to a lower schema version&lt;/li&gt;
&lt;li&gt;The fix (and why the obvious fix doesn't work)&lt;/li&gt;
&lt;li&gt;The rollback-window data problem: structural compatibility vs. data-semantic compatibility&lt;/li&gt;
&lt;li&gt;A bug that's easy to introduce while making the fix&lt;/li&gt;
&lt;li&gt;Testing rollback compatibility automatically in CI&lt;/li&gt;
&lt;li&gt;The git worktree workflow for building both versions side by side&lt;/li&gt;
&lt;li&gt;Building three test APKs so QA can test &lt;strong&gt;upgrade → rollback → re-upgrade&lt;/strong&gt; without losing data&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All names, branches and paths below are made up for the article.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Production&lt;/strong&gt; (&lt;code&gt;v1.4.1&lt;/code&gt;) uses database &lt;strong&gt;schema 11&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The new release&lt;/strong&gt; (&lt;code&gt;v1.5.0&lt;/code&gt;, branch &lt;code&gt;feature/new-release&lt;/code&gt;) moves to &lt;strong&gt;schema 12&lt;/strong&gt;. It adds three nullable columns:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;settings.dark_mode_enabled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;settings.beta_features_enabled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reports.submitted_at&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rollback build&lt;/strong&gt; (branch &lt;code&gt;release/1.4.2&lt;/code&gt;) is production code (schema 11) with a higher build number, so the stores treat it as an update.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The local database is &lt;a href="https://drift.simonbinder.eu/" rel="noopener noreferrer"&gt;Drift&lt;/a&gt; (version 2.x), and migrations use Drift's generated &lt;code&gt;stepByStep&lt;/code&gt; helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;schemaVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="n"&gt;MigrationStrategy&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;migration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MigrationStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;onCreate:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;seedDefaultSettings&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nl"&gt;onUpgrade:&lt;/span&gt; &lt;span class="n"&gt;stepByStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;from1To2:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nl"&gt;from10To11:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nl"&gt;beforeOpen:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;customStatement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'PRAGMA foreign_keys = ON'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;validateDatabaseSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// debug-only check&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;On a device that had run the schema 12 build, installing the schema 11 rollback build crashed on startup with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;StateError: runMigrationSteps was asked to downgrade from versions 12 to 11.
Migration steps can only upgrade. This might happen if users downgrade app versions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  First attempt: handle it in &lt;code&gt;beforeOpen&lt;/code&gt; (didn't work)
&lt;/h2&gt;

&lt;p&gt;My first idea was to detect the downgrade in &lt;code&gt;beforeOpen&lt;/code&gt;, then drop and recreate every table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;beforeOpen:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionBefore&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionBefore&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;schemaVersion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createMigrator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;customStatement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'PRAGMA foreign_keys = OFF'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;allTables&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;customStatement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'PRAGMA foreign_keys = ON'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It made no difference. The same error kept appearing, and the code inside the &lt;code&gt;if&lt;/code&gt; never ran.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why: Drift calls &lt;code&gt;onUpgrade&lt;/code&gt; before &lt;code&gt;beforeOpen&lt;/code&gt;, including for downgrades
&lt;/h3&gt;

&lt;p&gt;In Drift's source (&lt;code&gt;lib/src/runtime/api/db_base.dart&lt;/code&gt;), the open sequence looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wasCreated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_resolvedMigration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;migrator&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hadUpgrade&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;           &lt;span class="c1"&gt;// versionBefore != versionNow&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_resolvedMigration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onUpgrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;migrator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionBefore&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionNow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_resolvedMigration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;beforeOpen&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;hadUpgrade&lt;/code&gt; is just &lt;code&gt;versionBefore != versionNow&lt;/code&gt;. &lt;strong&gt;Drift doesn't distinguish upgrades from downgrades.&lt;/strong&gt; Both go through &lt;code&gt;onUpgrade&lt;/code&gt;, and &lt;code&gt;MigrationStrategy&lt;/code&gt;'s own docs say so: &lt;em&gt;"Schema version upgrades and downgrades will both be run here."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stepByStep&lt;/code&gt; then calls &lt;code&gt;VersionedSchema.runMigrationSteps&lt;/code&gt;, which refuses to downgrade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;StateError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'runMigrationSteps was asked to downgrade from versions &lt;/span&gt;&lt;span class="si"&gt;$from&lt;/span&gt;&lt;span class="s"&gt; to &lt;/span&gt;&lt;span class="si"&gt;$to&lt;/span&gt;&lt;span class="s"&gt;. ...'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the exception is thrown in &lt;code&gt;onUpgrade&lt;/code&gt;, the database fails to open, and &lt;code&gt;beforeOpen&lt;/code&gt; never runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why dropping everything was a bad idea anyway
&lt;/h3&gt;

&lt;p&gt;Even if it had worked, dropping every table and recreating it would have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lost unsynced data.&lt;/strong&gt; Anything the user created offline and hadn't uploaded yet would be gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logged users out&lt;/strong&gt;, if session or settings data lives in the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipped the default seed data.&lt;/strong&gt; &lt;code&gt;onCreate&lt;/code&gt; seeds default settings, but a bare &lt;code&gt;createAll()&lt;/code&gt; doesn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rollback is meant to make things safer for users, and wiping their data on the way back defeats that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the schema change actually was
&lt;/h2&gt;

&lt;p&gt;Before choosing a fix, I compared the two schemas. Schema 12 &lt;strong&gt;only added nullable columns&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That matters because Drift-generated queries always list columns explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nv"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"settings"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="nv"&gt;"reports"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An older build that doesn't know about &lt;code&gt;dark_mode_enabled&lt;/code&gt; never selects it, and on insert SQLite stores &lt;code&gt;NULL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, it is critical to distinguish between two kinds of compatibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structural (schema) compatibility:&lt;/strong&gt; Can the older application physically open the SQLite file, run its queries, and write rows without SQLite errors?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data-semantic compatibility:&lt;/strong&gt; When the newer application returns, will it correctly interpret data that was created or modified while the older application was running?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Extra nullable columns (or columns with defaults) are &lt;strong&gt;structurally compatible&lt;/strong&gt; with older code, provided the older application can safely ignore them. However, the newer application still needs to account for data written while those columns were unavailable.&lt;/p&gt;

&lt;p&gt;So for the immediate rollback step, the safest schema downgrade is a &lt;strong&gt;structural no-op&lt;/strong&gt;: keep the data intact and let Drift record the lower version number in SQLite's &lt;code&gt;PRAGMA user_version&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ This only works when the newer schema is &lt;em&gt;strictly additive&lt;/em&gt;. If it renamed or dropped columns, added &lt;code&gt;NOT NULL&lt;/code&gt; columns without defaults, or changed table shapes, you'd need a real downgrade migration (for example &lt;code&gt;m.alterTable(TableMigration(...))&lt;/code&gt; to rebuild the table).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Handle the downgrade in &lt;code&gt;onUpgrade&lt;/code&gt;, before &lt;code&gt;stepByStep&lt;/code&gt; runs
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;stepByStep(...)&lt;/code&gt; returns an &lt;code&gt;OnUpgrade&lt;/code&gt; function, &lt;code&gt;(Migrator m, int from, int to) =&amp;gt; Future&amp;lt;void&amp;gt;&lt;/code&gt;. Wrap it in your own closure and check the direction first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;onUpgrade:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Drift calls onUpgrade for downgrades too (from &amp;gt; to), e.g. when a user&lt;/span&gt;
  &lt;span class="c1"&gt;// on a newer build (schema 12) installs this rollback build (schema 11).&lt;/span&gt;
  &lt;span class="c1"&gt;// stepByStep only supports upgrades and throws on a downgrade, so skip it&lt;/span&gt;
  &lt;span class="c1"&gt;// and keep the existing data. Newer schemas only added nullable columns,&lt;/span&gt;
  &lt;span class="c1"&gt;// which this version ignores. Drift then writes user_version = `to`.&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Normal upgrade path. stepByStep returns an OnUpgrade function, so it&lt;/span&gt;
  &lt;span class="c1"&gt;// must be invoked with (m, from, to) and awaited.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stepByStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;from1To2:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nl"&gt;from10To11:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this runs, Drift sets &lt;code&gt;PRAGMA user_version&lt;/code&gt; to 11. The schema 12 columns are still in the file, but nothing reads them.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Skip debug schema validation after a downgrade
&lt;/h3&gt;

&lt;p&gt;The next run in debug mode produced a different error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Schema does not match
reports:
 columns:
  additional:
   Contains the following unexpected entries: submitted_at
settings:
 columns:
  additional:
   Contains the following unexpected entries: dark_mode_enabled, beta_features_enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This comes from &lt;code&gt;VerifySelf(db).validateDatabaseSchema()&lt;/code&gt; (from &lt;code&gt;drift_dev&lt;/code&gt;), which compares the real tables with the current schema. That check is usually wrapped in &lt;code&gt;kDebugMode&lt;/code&gt;, so &lt;strong&gt;it doesn't affect release builds&lt;/strong&gt;, but it will get in your way when testing locally. Skip it only when the database was downgraded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;beforeOpen:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;customStatement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'PRAGMA foreign_keys = ON'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// After a downgrade the tables still have the newer schema's columns, so&lt;/span&gt;
  &lt;span class="c1"&gt;// the debug-only schema validation would fail. Skip it in that case;&lt;/span&gt;
  &lt;span class="c1"&gt;// fresh installs and upgrades are still validated in debug builds.&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;isDowngrade&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionBefore&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionBefore&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionNow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isDowngrade&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;validateDatabaseSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Make the newer migration safe to run twice
&lt;/h3&gt;

&lt;p&gt;When a user goes &lt;strong&gt;12 → 11 → 12&lt;/strong&gt;, &lt;code&gt;from11To12&lt;/code&gt; runs &lt;strong&gt;a second time&lt;/strong&gt; against a database that already has those columns. A plain &lt;code&gt;addColumn&lt;/code&gt; would fail with &lt;code&gt;duplicate column name&lt;/code&gt;. Guard it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_columnExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;customSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'PRAGMA table_info(&lt;/span&gt;&lt;span class="si"&gt;$table&lt;/span&gt;&lt;span class="s"&gt;)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;any&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nl"&gt;from11To12:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_columnExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'settings'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'dark_mode_enabled'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settingsTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settingsTable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;darkModeEnabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...same for the other columns&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ideally the migration in the &lt;strong&gt;new release&lt;/strong&gt; is written this way from the start. It costs very little and makes a rollback much safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rollback-window data problem: structural vs. semantic compatibility
&lt;/h2&gt;

&lt;p&gt;Making the database structurally downgradeable prevents the crash on startup, but it exposes a deeper challenge: &lt;strong&gt;the semantic data gap&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Consider the complete round-trip lifecycle of a rollback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v12 (new release)
  │
  ▼  Rollback triggered
v11 (rollback build active)
  │
  ▼  User creates, edits, or submits data
v11 writes rows (v12-only columns are stored as NULL)
  │
  ▼  Fixed v12 re-shipped
v12 (re-upgrade)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any record created or modified while the user is on the v11 rollback build has no awareness of schema 12's columns.&lt;/p&gt;

&lt;p&gt;Take &lt;code&gt;reports.submitted_at&lt;/code&gt; as a concrete example.&lt;/p&gt;

&lt;p&gt;In v11, the &lt;code&gt;reports&lt;/code&gt; table exists, but v11 queries have no reference to &lt;code&gt;submitted_at&lt;/code&gt;. If a user submits a report while running v11, the row is inserted or updated without touching &lt;code&gt;submitted_at&lt;/code&gt;. In SQLite, that column remains &lt;code&gt;NULL&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;report_id | title          | submitted_at
----------|----------------|-------------
123       | Q3 Audit       | NULL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the user eventually re-upgrades to v12, what does &lt;code&gt;submitted_at == NULL&lt;/code&gt; mean?&lt;/p&gt;

&lt;p&gt;In normal v12 code, developers often assume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submittedAt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// "This report has not been submitted yet"&lt;/span&gt;
  &lt;span class="n"&gt;showDraftStatus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="n"&gt;showSubmitButton&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That assumption is dangerous. In a post-rollback world, &lt;code&gt;submitted_at = NULL&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; necessarily mean "not submitted." It may mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This record was created or modified while the application was running an older schema during the rollback window.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Blindly treating &lt;code&gt;NULL&lt;/code&gt; as a definitive business state can trigger serious defects: a report the customer successfully submitted under v11 suddenly reappears as a draft, or a background sync process submits the report to your server a second time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two ways to solve the NULL/unknown problem
&lt;/h3&gt;

&lt;p&gt;Depending on your domain and data model, there are two approaches to address this:&lt;/p&gt;

&lt;h4&gt;
  
  
  Approach A — Backfill during the re-upgrade migration
&lt;/h4&gt;

&lt;p&gt;If the existing v11 data contains enough surrounding information to reconstruct or infer the correct state, &lt;code&gt;from11To12&lt;/code&gt; can backfill the column when the user returns to v12:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;from11To12:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_columnExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'reports'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'submitted_at'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reportsTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reportsTable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submittedAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Backfill from existing v11 state where possible.&lt;/span&gt;
  &lt;span class="c1"&gt;// The exact conditions depend entirely on your application's data model&lt;/span&gt;
  &lt;span class="c1"&gt;// (for example, checking an existing status column or an audit log).&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;customStatement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'''
    UPDATE reports
    SET submitted_at = ...
    WHERE submitted_at IS NULL
      AND ...;
  '''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your schema already has an existing status column or sync timestamp from schema 11, you can use that to backfill a sensible value. The exact SQL depends on your specific data model, but the key is placing it inside the idempotent &lt;code&gt;from11To12&lt;/code&gt; migration step.&lt;/p&gt;

&lt;h4&gt;
  
  
  Approach B — Treat NULL as "unknown" in application logic
&lt;/h4&gt;

&lt;p&gt;If v11 simply does not capture enough data to reconstruct the missing value, the v12 application logic should treat &lt;code&gt;NULL&lt;/code&gt; as &lt;strong&gt;unknown&lt;/strong&gt;, rather than assuming "no":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submittedAt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Unknown: this record may have passed through an older schema&lt;/span&gt;
  &lt;span class="c1"&gt;// during a rollback window. Reconcile with the server or inspect&lt;/span&gt;
  &lt;span class="c1"&gt;// secondary state before allowing re-submission.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;reconcileReportStatusWithServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;showSubmittedConfirmation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submittedAt&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treating &lt;code&gt;NULL&lt;/code&gt; as an ambiguous state prompts your app to check secondary signals — querying your sync server, inspecting local event logs, or running a reconciliation check — before taking a destructive or user-visible action.&lt;/p&gt;

&lt;p&gt;Neither approach is universal; the right choice depends on your business domain. What matters is that your v12 code does not mistake a missing column value for a definitive business state.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bug that's easy to introduce while making the fix
&lt;/h2&gt;

&lt;p&gt;My first version of the wrapper looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;onUpgrade:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;stepByStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;          &lt;span class="c1"&gt;// ❌ no await, and never called&lt;/span&gt;
    &lt;span class="nl"&gt;from1To2:&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compiles, and the downgrade test passes because it returns before reaching that line. But &lt;code&gt;stepByStep(...)&lt;/code&gt; only &lt;strong&gt;builds&lt;/strong&gt; the migration function. It's never called, so &lt;strong&gt;every normal upgrade does nothing&lt;/strong&gt;. Drift still writes the new &lt;code&gt;user_version&lt;/code&gt;, leaving users on older schemas with missing columns.&lt;/p&gt;

&lt;p&gt;The correct form is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;stepByStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; after touching &lt;code&gt;onUpgrade&lt;/code&gt;, always run your migration tests for the &lt;em&gt;upgrade&lt;/em&gt; path, not just the scenario you were fixing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flutter &lt;span class="nb"&gt;test test&lt;/span&gt;/drift/migration_test.dart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing locally without the release keystore
&lt;/h2&gt;

&lt;p&gt;Release signing lived in CI, so &lt;code&gt;flutter run --release&lt;/code&gt; wasn't possible locally. Two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Profile mode.&lt;/strong&gt; It isn't &lt;code&gt;kDebugMode&lt;/code&gt;, so it behaves like release for this test:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  flutter run &lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nt"&gt;--flavor&lt;/span&gt; production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Android complains about signing, temporarily point the build type at the debug signing config. &lt;strong&gt;Don't commit that change.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debug mode&lt;/strong&gt; with the validation guard from step 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Checking that the downgrade happened
&lt;/h3&gt;

&lt;p&gt;Log the versions in &lt;code&gt;beforeOpen&lt;/code&gt;. Use &lt;code&gt;print&lt;/code&gt;, because &lt;code&gt;kDebugMode&lt;/code&gt;-guarded logging doesn't appear in profile mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'[DB] versionBefore=&lt;/span&gt;&lt;span class="si"&gt;${details.versionBefore}&lt;/span&gt;&lt;span class="s"&gt; versionNow=&lt;/span&gt;&lt;span class="si"&gt;${details.versionNow}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;first launch of the rollback: &lt;code&gt;versionBefore=12 versionNow=11&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;next launch: &lt;code&gt;versionBefore=11 versionNow=11&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or check the file directly on Android:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb exec-out run-as com.example.myapp &lt;span class="nb"&gt;cat &lt;/span&gt;app_flutter/app.db &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; app.db
sqlite3 app.db &lt;span class="s2"&gt;"PRAGMA user_version;"&lt;/span&gt;            &lt;span class="c"&gt;# expect 11&lt;/span&gt;
sqlite3 app.db &lt;span class="s2"&gt;"PRAGMA table_info(settings);"&lt;/span&gt;    &lt;span class="c"&gt;# schema 12 columns still there, unused&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;getApplicationDocumentsDirectory()&lt;/code&gt; maps to &lt;code&gt;app_flutter/&lt;/code&gt; on Android. If the app is still running, also pull &lt;code&gt;app.db-wal&lt;/code&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The git workflow: two versions side by side with &lt;code&gt;git worktree&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;To test the round trip you need builds from &lt;strong&gt;two branches&lt;/strong&gt;. Your working branch probably has uncommitted local files (environment config, FVM settings and so on), so switching back and forth is painful. &lt;code&gt;git worktree&lt;/code&gt; checks another branch out into a &lt;strong&gt;separate folder&lt;/strong&gt; that uses the same repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# from the repo root&lt;/span&gt;
git worktree list
git worktree add ../myapp-v12 feature/new-release
&lt;span class="nb"&gt;cd&lt;/span&gt; ../myapp-v12

&lt;span class="c"&gt;# confirm it's really the newer schema&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"schemaVersion =&amp;gt;"&lt;/span&gt; lib/data/database/database.dart   &lt;span class="c"&gt;# expect 12&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Untracked local files aren't in git, so copy them across:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; ../myapp/.fvmrc &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; ../myapp/lib/core/env.dart lib/core/      &lt;span class="c"&gt;# make sure both builds use the same backend environment&lt;/span&gt;
fvm &lt;span class="nb"&gt;install
&lt;/span&gt;fvm flutter pub get
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One rule: &lt;strong&gt;a branch can only be checked out in one worktree at a time.&lt;/strong&gt; If you get &lt;code&gt;fatal: '&amp;lt;branch&amp;gt;' is already checked out&lt;/code&gt;, &lt;code&gt;git worktree list&lt;/code&gt; shows where it is.&lt;/p&gt;

&lt;p&gt;When you're done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree remove ../myapp-v12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building the three QA builds
&lt;/h2&gt;

&lt;p&gt;QA needs to test three transitions on &lt;strong&gt;one device without uninstalling&lt;/strong&gt;, because uninstalling deletes the database and makes the test pointless.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;APK&lt;/th&gt;
&lt;th&gt;Branch&lt;/th&gt;
&lt;th&gt;Schema&lt;/th&gt;
&lt;th&gt;Build number&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;feature/new-release&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;The new release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;release/1.4.2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;151&lt;/td&gt;
&lt;td&gt;The rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;feature/new-release&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;152&lt;/td&gt;
&lt;td&gt;Re-upgrade after the rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There are two Android rules to work around:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Same signing key.&lt;/strong&gt; Android won't install an update signed with a different key (&lt;code&gt;INSTALL_FAILED_UPDATE_INCOMPATIBLE&lt;/code&gt;). Build all three on the &lt;strong&gt;same machine&lt;/strong&gt;, so they're all signed with &lt;code&gt;~/.android/debug.keystore&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;versionCode&lt;/code&gt; must not go down&lt;/strong&gt; when the user taps an APK to install it. That's why &lt;strong&gt;C&lt;/strong&gt; exists: the same code as A, with a higher build number, so QA can install it over B. Without C, QA needs &lt;code&gt;adb install -r -d A.apk&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Commands
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/Desktop/QA-ROLLBACK
&lt;span class="nv"&gt;FLAVOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production
&lt;span class="nv"&gt;APK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;build/app/outputs/flutter-apk/app-&lt;span class="nv"&gt;$FLAVOR&lt;/span&gt;&lt;span class="nt"&gt;-debug&lt;/span&gt;.apk
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# B: the rollback (main folder, release/1.4.2)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/dev/myapp
fvm flutter clean &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; fvm flutter pub get
fvm flutter build apk &lt;span class="nt"&gt;--debug&lt;/span&gt; &lt;span class="nt"&gt;--flavor&lt;/span&gt; &lt;span class="nv"&gt;$FLAVOR&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;/B_rollback_v1.4.2-151_db11_debug.apk"&lt;/span&gt;

&lt;span class="c"&gt;# A: the new release (worktree, feature/new-release)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/dev/myapp-v12
fvm flutter pub get
fvm flutter build apk &lt;span class="nt"&gt;--debug&lt;/span&gt; &lt;span class="nt"&gt;--flavor&lt;/span&gt; &lt;span class="nv"&gt;$FLAVOR&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;/A_new-release_v1.5.0-150_db12_debug.apk"&lt;/span&gt;

&lt;span class="c"&gt;# C: re-upgrade (same worktree, higher build number)&lt;/span&gt;
fvm flutter build apk &lt;span class="nt"&gt;--debug&lt;/span&gt; &lt;span class="nt"&gt;--flavor&lt;/span&gt; &lt;span class="nv"&gt;$FLAVOR&lt;/span&gt; &lt;span class="nt"&gt;--build-number&lt;/span&gt; 152
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;/C_reupgrade_v1.5.0-152_db12_debug.apk"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Putting the order, version and schema in the file names makes it hard for QA to install them in the wrong order.&lt;/p&gt;

&lt;p&gt;Debug APKs include every CPU architecture and can be 150–250 MB. If your ticket system rejects them, build for physical phones only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fvm flutter build apk &lt;span class="nt"&gt;--debug&lt;/span&gt; &lt;span class="nt"&gt;--flavor&lt;/span&gt; &lt;span class="nv"&gt;$FLAVOR&lt;/span&gt; &lt;span class="nt"&gt;--target-platform&lt;/span&gt; android-arm64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The QA test plan
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Install order: A → B → C. Never uninstall in between.&lt;/strong&gt; If a store build is already on the device, uninstall it &lt;em&gt;once&lt;/em&gt; before A, because it's signed with a different key.&lt;/p&gt;

&lt;p&gt;A successful database migration only proves that the database engine could open the file without throwing a SQLite error. It does &lt;strong&gt;not&lt;/strong&gt; prove that data written during the rollback window still has the correct business meaning. QA must test both structural and data-semantic transitions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install v12 (Build A).&lt;/strong&gt; Log in, go offline, create unsynced data (e.g. drafts or reports), and change settings (&lt;code&gt;dark_mode_enabled&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback to v11 (A → B).&lt;/strong&gt; Install B directly over A without uninstalling.

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Expected:&lt;/em&gt; The app opens cleanly, the user is still logged in, and all existing data remains accessible.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the app during the rollback window.&lt;/strong&gt; On B, create and modify records in the tables that contain v12 columns (for example, submit a new report on B where &lt;code&gt;submitted_at&lt;/code&gt; is not referenced by code). Change a setting. Then go back online.

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Expected:&lt;/em&gt; Saving works and records sync. SQLite writes &lt;code&gt;NULL&lt;/code&gt; for the unused v12 columns.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-upgrade to v12 (B → C).&lt;/strong&gt; Install C directly over B without uninstalling.

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Expected:&lt;/em&gt;

&lt;ul&gt;
&lt;li&gt;No migration error occurs (the guarded &lt;code&gt;from11To12&lt;/code&gt; skips existing columns instead of failing with "duplicate column name").&lt;/li&gt;
&lt;li&gt;Existing data created under A remains intact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Critically, verify the records created or modified under B:&lt;/strong&gt; Verify that v12-only fields on those records are interpreted correctly. Verify that &lt;code&gt;NULL&lt;/code&gt; values are not blindly treated as a definitive business state (e.g. submitted reports should not be reverted to drafts or re-submitted).&lt;/li&gt;
&lt;li&gt;Verify that records are not duplicated, corrupted, or shown in an inconsistent state.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regression: production → B.&lt;/strong&gt; Install the current store build (schema 11), then B over it.

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Expected:&lt;/em&gt; An 11 → 11 no-op with all data intact.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fresh install of B.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Expected:&lt;/em&gt; &lt;code&gt;onCreate&lt;/code&gt; runs and default settings are seeded.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Put rollback compatibility in CI
&lt;/h2&gt;

&lt;p&gt;Manual QA on physical devices is indispensable, but the "additive-only" constraint should not rely solely on a manual QA checklist. A developer working on a future schema could easily introduce a breaking change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;reports&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;reviewer_notes&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- ❌ Breaks downgrade!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or rename an existing column, silently making future rollbacks impossible.&lt;/p&gt;

&lt;p&gt;Catch this before release by automating rollback compatibility in CI using &lt;code&gt;drift_dev&lt;/code&gt;'s schema snapshots and &lt;code&gt;SchemaVerifier&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Drift allows dumping schema snapshots to JSON files via &lt;code&gt;drift_dev make-migrations&lt;/code&gt;. In your test suite, you can write an automated integration test that simulates the complete round-trip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v11 database
    ↓
open/use with v12
    ↓
v12 database
    ↓
open/use with v11 (rollback)
    ↓
upgrade again to v12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;SchemaVerifier&lt;/code&gt;, you can verify that the database class for v11 can cleanly open a database initialized with schema 12, and that re-opening it with v12 succeeds without errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// test/drift/rollback_test.dart&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:drift_dev/api/migrations.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:test/test.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'schema 12 database can safely be opened by schema 11 and re-upgraded'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;verifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SchemaVerifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GeneratedHelper&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Start with schema 11, upgrade to 12&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;verifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;dbV12&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbV12&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;customStatement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'PRAGMA user_version = 12'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbV12&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Open the v12 database using the v11 rollback database class&lt;/span&gt;
    &lt;span class="c1"&gt;// This exercises the `if (from &amp;gt; to) return;` guard in onUpgrade.&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;rollbackConnection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;verifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newConnection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;dbV11Rollback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyRollbackDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rollbackConnection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dbV11Rollback&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbV11Rollback&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reports&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;returnsNormally&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbV11Rollback&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Re-upgrade back to v12&lt;/span&gt;
    &lt;span class="c1"&gt;// This exercises the idempotent from11To12 migration step.&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;reUpgradeConnection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;verifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newConnection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;dbV12Reopened&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reUpgradeConnection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dbV12Reopened&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbV12Reopened&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reports&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;returnsNormally&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbV12Reopened&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this in your pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flutter &lt;span class="nb"&gt;test test&lt;/span&gt;/drift/rollback_test.dart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This automated check validates the schema contract: if an incompatible schema change (such as a &lt;code&gt;NOT NULL&lt;/code&gt; column without a default) is introduced, the test fails immediately in CI before the release is ever cut.&lt;/p&gt;

&lt;p&gt;Remember: &lt;strong&gt;CI validates the migration contract; device testing validates the real release build.&lt;/strong&gt; CI verifies that SQLite will accept the schema; the three-APK device test verifies that package management, background tasks, and application-level business state behave properly in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping it
&lt;/h2&gt;

&lt;p&gt;Commit &lt;strong&gt;only&lt;/strong&gt; the database change. Generated plugin registrants, rebuilt web workers and local config files can stay out of the commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add lib/data/database/database.dart
git status &lt;span class="nt"&gt;--short&lt;/span&gt;     &lt;span class="c"&gt;# only this file should be staged&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Allow safe DB downgrade from schema 12 to 11 for rollback build"&lt;/span&gt;
git push origin release/1.4.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch for branches whose names differ only in case (&lt;code&gt;release/1.4.2&lt;/code&gt; vs &lt;code&gt;Release/1.4.2&lt;/code&gt;). Check what your branch tracks before you push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rev-parse &lt;span class="nt"&gt;--abbrev-ref&lt;/span&gt; @&lt;span class="o"&gt;{&lt;/span&gt;u&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When CI builds the store version of the rollback, its &lt;strong&gt;build number must be higher than the new release's&lt;/strong&gt;, or the stores won't offer it as an update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drift sends downgrades through &lt;code&gt;onUpgrade&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;beforeOpen&lt;/code&gt; runs afterwards, so it's too late to handle a downgrade there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;stepByStep&lt;/code&gt; throws on a downgrade.&lt;/strong&gt; Wrap it and check &lt;code&gt;from &amp;gt; to&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema compatibility is not data-semantic compatibility.&lt;/strong&gt; An older build opening a newer SQLite database without error does not mean the newer app will understand data written during the rollback window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data written during the rollback window must be planned for.&lt;/strong&gt; New nullable columns will contain &lt;code&gt;NULL&lt;/code&gt;. Either backfill them in your re-upgrade migration or treat &lt;code&gt;NULL&lt;/code&gt; as an explicit "unknown" state in application logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the newer schema is additive, the safest downgrade is a no-op.&lt;/strong&gt; Keep the data. Only drop or rebuild tables when an incompatible schema change makes it unavoidable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make newer migrations idempotent&lt;/strong&gt; (&lt;code&gt;_columnExists&lt;/code&gt; checks) so the 12 → 11 → 12 cycle does not crash with "duplicate column name".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip debug-only schema validation&lt;/strong&gt; after a downgrade so local development and QA aren't blocked by the extra columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;stepByStep(...)&lt;/code&gt; returns a function.&lt;/strong&gt; Call it and &lt;code&gt;await&lt;/code&gt; it, and always run your upgrade tests after editing &lt;code&gt;onUpgrade&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce rollback compatibility in CI.&lt;/strong&gt; Use Drift schema snapshots and &lt;code&gt;SchemaVerifier&lt;/code&gt; to catch non-additive or breaking schema changes before release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git worktree&lt;/code&gt; + same-key debug builds + build-number control&lt;/strong&gt; lets QA test the entire upgrade → rollback → re-upgrade cycle on one device without uninstalling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback safety is a multi-layered discipline.&lt;/strong&gt; It requires coordinating schema structure, migration idempotency, semantic business logic, and automated CI tests before release day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your next release adds anything other than nullable columns, plan the downgrade path &lt;strong&gt;before&lt;/strong&gt; you ship it.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>mobile</category>
      <category>drift</category>
    </item>
    <item>
      <title>Shorebird: Deploy Flutter Updates Without App Store Review (Step-by-Step Guide)</title>
      <dc:creator>George Ikwegbu Chinedu</dc:creator>
      <pubDate>Wed, 23 Sep 2026 00:05:58 +0000</pubDate>
      <link>https://dev.to/gikwegbu/shorebird-deploy-flutter-updates-without-app-store-review-step-by-step-guide-155c</link>
      <guid>https://dev.to/gikwegbu/shorebird-deploy-flutter-updates-without-app-store-review-step-by-step-guide-155c</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What is Shorebird?&lt;/li&gt;
&lt;li&gt;Why You Need It&lt;/li&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;Installation&lt;/li&gt;
&lt;li&gt;Setup &amp;amp; Configuration&lt;/li&gt;
&lt;li&gt;Creating Your First Release&lt;/li&gt;
&lt;li&gt;Deploying Patches&lt;/li&gt;
&lt;li&gt;CI/CD Integration&lt;/li&gt;
&lt;li&gt;Best Practices&lt;/li&gt;
&lt;li&gt;Troubleshooting&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What is Shorebird?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://shorebird.dev/" rel="noopener noreferrer"&gt;Shorebird&lt;/a&gt; is a &lt;strong&gt;code push solution for Flutter&lt;/strong&gt; that lets you update your app's Dart code &lt;em&gt;without&lt;/em&gt; going through the Apple App Store or Google Play Store review process.&lt;/p&gt;

&lt;p&gt;Think of it as hotfix superpowers for production apps. Bug in production? Deploy a patch in minutes instead of waiting days for app store approval.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Capabilities:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ Push Dart code updates instantly&lt;/li&gt;
&lt;li&gt;✅ Bypass app store review cycles&lt;/li&gt;
&lt;li&gt;✅ Automated rollback on crashes&lt;/li&gt;
&lt;li&gt;✅ Staged rollouts (release to 5%, then 50%, then 100%)&lt;/li&gt;
&lt;li&gt;✅ Detailed analytics on patch adoption&lt;/li&gt;
&lt;li&gt;✅ Works with Flutter's native capabilities&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why You Need It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Scenario:&lt;/strong&gt; It's Friday evening. Your production app has a critical bug affecting checkout. Users are losing money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without Shorebird:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Submit fix to Apple App Store&lt;/li&gt;
&lt;li&gt;Wait 24-48 hours for review&lt;/li&gt;
&lt;li&gt;Users suffer the entire time&lt;/li&gt;
&lt;li&gt;Potential revenue loss&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;With Shorebird:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy fix in 5 minutes&lt;/li&gt;
&lt;li&gt;Users get the patch immediately&lt;/li&gt;
&lt;li&gt;Crisis averted&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Real-World Benefits:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reduce Time-to-Market:&lt;/strong&gt; Deploy hotfixes without waiting for store approval&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better User Experience:&lt;/strong&gt; Critical bugs fixed instantly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost Savings:&lt;/strong&gt; Fewer production incidents = fewer support tickets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster Iteration:&lt;/strong&gt; Deploy A/B tests and feature toggles in real-time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD Integration:&lt;/strong&gt; Automate patch deployment through your pipeline&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before starting, ensure you have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Flutter Project:&lt;/strong&gt; An existing Flutter app (or create one)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   flutter create my_app
   &lt;span class="nb"&gt;cd &lt;/span&gt;my_app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Shorebird Account:&lt;/strong&gt; Sign up at &lt;a href="https://shorebird.dev/" rel="noopener noreferrer"&gt;shorebird.dev&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flutter SDK:&lt;/strong&gt; Version 3.0 or higher&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   flutter &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Git:&lt;/strong&gt; For version control
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   git &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Platform Requirements:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;iOS:&lt;/strong&gt; Xcode 14+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android:&lt;/strong&gt; Android SDK 21+&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Install Shorebird CLI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On macOS/Linux&lt;/span&gt;
curl &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=https'&lt;/span&gt; &lt;span class="nt"&gt;--tlsv1&lt;/span&gt;.2 https://raw.githubusercontent.com/shorebirdtech/shorebird/main/scripts/install.sh | bash

&lt;span class="c"&gt;# On Windows (PowerShell)&lt;/span&gt;
iwr https://raw.githubusercontent.com/shorebirdtech/shorebird/main/scripts/install.ps1 &lt;span class="nt"&gt;-UseBasicParsing&lt;/span&gt; | iex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Verify Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shorebird &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shorebird 0.x.x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Initialize Shorebird in Your Project
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;your_flutter_project
shorebird init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.shorebird/config.yaml&lt;/code&gt; - Shorebird configuration&lt;/li&gt;
&lt;li&gt;Updates &lt;code&gt;pubspec.yaml&lt;/code&gt; with dependencies&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Setup &amp;amp; Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Authenticate with Shorebird
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shorebird login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens a browser window for authentication. Sign in with your Shorebird account.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Review &lt;code&gt;shorebird.yaml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Located in your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;app_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-app-id-here"&lt;/span&gt;
&lt;span class="na"&gt;flavors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;app_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod-app-id"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Update Your App Version
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;pubspec.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.0.0+1&lt;/span&gt;  &lt;span class="c1"&gt;# version+build_number&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Increment the build number for each Shorebird release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Configure Your App
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;lib/main.dart&lt;/code&gt;, ensure your app can handle code updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:shorebird_code_push/shorebird_code_push.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;WidgetsFlutterBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ensureInitialized&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Optional: Check for updates on app start&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;codePush&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ShorebirdsCodePush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;codePush&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;checkForUpdates&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Creating Your First Release
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Build for Release
&lt;/h3&gt;

&lt;p&gt;Before creating a Shorebird release, build your app for both platforms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Android:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shorebird build apk &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For iOS:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shorebird build ipa &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Review Build Output
&lt;/h3&gt;

&lt;p&gt;Successful output looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;✓ Built APK: build/app/outputs/flutter-app-release.apk
✓ Built IPA: build/ios/ipa/MyApp.ipa
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Submit to App Stores
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Google Play Store:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fastlane supply &lt;span class="nt"&gt;--apk&lt;/span&gt; build/app/outputs/flutter-app-release.apk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or upload manually via Google Play Console.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apple App Store:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fastlane pilot upload &lt;span class="nt"&gt;--ipa&lt;/span&gt; build/ios/ipa/MyApp.ipa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use Xcode/Transporter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Create a Release in Shorebird
&lt;/h3&gt;

&lt;p&gt;Once your app is live on stores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shorebird release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This captures the current code state as your baseline release.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deploying Patches
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scenario: You have a bug fix ready
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Step 1: Make Your Code Changes
&lt;/h3&gt;

&lt;p&gt;Fix the bug in your Dart code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoginScreen&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// BUG: Email validation broken&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onChanged:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emailValidation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoginScreen&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// FIXED: Proper email validation&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onChanged:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_validateEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_validateEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;regex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sx"&gt;r'^[^@]+@[^@]+\.[^@]+'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Bump the Build Number
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;pubspec.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.0.0+1&lt;/span&gt;

&lt;span class="c1"&gt;# After&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.0.0+2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Create a Patch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shorebird patch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shorebird will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build the patched code&lt;/li&gt;
&lt;li&gt;Compare it to the release baseline&lt;/li&gt;
&lt;li&gt;Create a minimal delta package&lt;/li&gt;
&lt;li&gt;Upload it to Shorebird servers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Monitor Patch Status
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shorebird patch status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Patch creation status&lt;/li&gt;
&lt;li&gt;Percentage of devices updated&lt;/li&gt;
&lt;li&gt;Any errors or rollbacks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5: Staged Rollout (Optional)
&lt;/h3&gt;

&lt;p&gt;Deploy to a small percentage first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shorebird patch &lt;span class="nt"&gt;--staged-rollout&lt;/span&gt; 0.05
&lt;span class="c"&gt;# Deploys to 5% of users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Monitor for 24 hours, then increase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shorebird patch &lt;span class="nt"&gt;--staged-rollout&lt;/span&gt; 0.5
&lt;span class="c"&gt;# Deploys to 50% of users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, roll out to everyone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shorebird patch &lt;span class="nt"&gt;--staged-rollout&lt;/span&gt; 1.0
&lt;span class="c"&gt;# 100% rollout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  CI/CD Integration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GitHub Actions Example
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/shorebird-patch.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Shorebird Patch Deployment&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;staged_rollout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Staged&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rollout&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;percentage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(0.0-1.0)'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.0'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;subosito/flutter-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;flutter-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.x'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Shorebird&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl --proto '=https' --tlsv1.2 https://raw.githubusercontent.com/shorebirdtech/shorebird/main/scripts/install.sh | bash&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Authenticate Shorebird&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shorebird login --token ${{ secrets.SHOREBIRD_TOKEN }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create Patch&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shorebird patch --staged-rollout ${{ github.event.inputs.staged_rollout || '1.0' }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Notify Slack&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slackapi/slack-github-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;webhook-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SLACK_WEBHOOK }}&lt;/span&gt;
          &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;{&lt;/span&gt;
              &lt;span class="s"&gt;"text": "✅ Shorebird patch deployed successfully!",&lt;/span&gt;
              &lt;span class="s"&gt;"blocks": [&lt;/span&gt;
                &lt;span class="s"&gt;{&lt;/span&gt;
                  &lt;span class="s"&gt;"type": "section",&lt;/span&gt;
                  &lt;span class="s"&gt;"text": {&lt;/span&gt;
                    &lt;span class="s"&gt;"type": "mrkdwn",&lt;/span&gt;
                    &lt;span class="s"&gt;"text": "Shorebird Patch Deployed\nRollout: ${{ github.event.inputs.staged_rollout || '100%' }}\nRef: ${{ github.ref }}"&lt;/span&gt;
                  &lt;span class="s"&gt;}&lt;/span&gt;
                &lt;span class="s"&gt;}&lt;/span&gt;
              &lt;span class="s"&gt;]&lt;/span&gt;
            &lt;span class="s"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GitLab CI Example
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;patch:shorebird&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google/flutter:latest&lt;/span&gt;

  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl --proto '=https' --tlsv1.2 https://raw.githubusercontent.com/shorebirdtech/shorebird/main/scripts/install.sh | bash&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;shorebird login --token $SHOREBIRD_TOKEN&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;shorebird patch --staged-rollout &lt;/span&gt;&lt;span class="m"&gt;0.5&lt;/span&gt;

  &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

  &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Test Before Patching&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Always test your code locally and in staging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flutter &lt;span class="nb"&gt;test
&lt;/span&gt;flutter analyze
flutter run &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. &lt;strong&gt;Use Semantic Versioning&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Major.Minor.Patch+BuildNumber&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.2.3+45&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. &lt;strong&gt;Document Your Patches&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Create a CHANGELOG:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## [1.2.3] - 2024-06-07&lt;/span&gt;

&lt;span class="gu"&gt;### Fixed&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Fixed email validation bug in login screen
&lt;span class="p"&gt;-&lt;/span&gt; Corrected typo in onboarding flow
&lt;span class="p"&gt;-&lt;/span&gt; Resolved memory leak in metrics tracker

&lt;span class="gu"&gt;### Changed&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Improved error messages for better UX
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. &lt;strong&gt;Staged Rollouts for Critical Patches&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Never roll out 100% immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Day 1: 5%&lt;/span&gt;
shorebird patch &lt;span class="nt"&gt;--staged-rollout&lt;/span&gt; 0.05

&lt;span class="c"&gt;# Day 2: 25%&lt;/span&gt;
shorebird patch &lt;span class="nt"&gt;--staged-rollout&lt;/span&gt; 0.25

&lt;span class="c"&gt;# Day 3: 100%&lt;/span&gt;
shorebird patch &lt;span class="nt"&gt;--staged-rollout&lt;/span&gt; 1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. &lt;strong&gt;Monitor Metrics&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Check adoption and error rates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shorebird patch status &lt;span class="nt"&gt;--verbose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. &lt;strong&gt;Have a Rollback Plan&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If a patch causes issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shorebird patch rollback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Issue: "App ID not found"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shorebird init  &lt;span class="c"&gt;# Re-initialize&lt;/span&gt;
shorebird login  &lt;span class="c"&gt;# Re-authenticate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issue: "Patch failed: No changes detected"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; No Dart code changes since last release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ensure build number is incremented&lt;/span&gt;
version: 1.0.0+2  &lt;span class="c"&gt;# Changed from +1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issue: "Users not receiving patch"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; App not checking for updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Add to &lt;code&gt;main.dart&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;codePush&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ShorebirdsCodePush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;codePush&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;checkForUpdates&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issue: "Staged rollout stuck at X%"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shorebird patch status &lt;span class="nt"&gt;--verbose&lt;/span&gt;
shorebird patch resume  &lt;span class="c"&gt;# Resume rollout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issue: "Build fails on iOS"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;ios
pod repo update
pod &lt;span class="nb"&gt;install
cd&lt;/span&gt; ..
flutter clean
shorebird build ipa &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Shorebird transforms how you deploy Flutter apps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Without Shorebird&lt;/th&gt;
&lt;th&gt;With Shorebird&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hotfix Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;24-48 hours&lt;/td&gt;
&lt;td&gt;5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User Experience&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bugs persist&lt;/td&gt;
&lt;td&gt;Instant fixes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A/B Testing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires app update&lt;/td&gt;
&lt;td&gt;Real-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rollback&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires new submission&lt;/td&gt;
&lt;td&gt;Instant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (support tickets)&lt;/td&gt;
&lt;td&gt;Low (automated)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Visit:&lt;/strong&gt; &lt;a href="https://shorebird.dev/" rel="noopener noreferrer"&gt;https://shorebird.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read:&lt;/strong&gt; Shorebird documentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try:&lt;/strong&gt; Create your first patch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrate:&lt;/strong&gt; Add to your CI/CD pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy:&lt;/strong&gt; Ship faster, update smarter&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;📖 &lt;strong&gt;Official Docs:&lt;/strong&gt; &lt;a href="https://docs.shorebird.dev/" rel="noopener noreferrer"&gt;https://docs.shorebird.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐛 &lt;strong&gt;GitHub Issues:&lt;/strong&gt; &lt;a href="https://github.com/shorebirdtech/shorebird" rel="noopener noreferrer"&gt;https://github.com/shorebirdtech/shorebird&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Discord Community:&lt;/strong&gt; &lt;a href="https://discord.gg/shorebird" rel="noopener noreferrer"&gt;https://discord.gg/shorebird&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📺 &lt;strong&gt;Video Guide:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=EIiBDoVHlNc&amp;amp;t=10s" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=EIiBDoVHlNc&amp;amp;t=10s&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Happy patching! 🚀&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>shorebird</category>
    </item>
    <item>
      <title>📲 Why Your iOS Push Notifications Just Died: APNs Certificates vs. Keys</title>
      <dc:creator>George Ikwegbu Chinedu</dc:creator>
      <pubDate>Mon, 04 May 2026 16:16:08 +0000</pubDate>
      <link>https://dev.to/gikwegbu/why-your-ios-push-notifications-just-died-apns-certificates-vs-keys-4efa</link>
      <guid>https://dev.to/gikwegbu/why-your-ios-push-notifications-just-died-apns-certificates-vs-keys-4efa</guid>
      <description>&lt;p&gt;&lt;em&gt;Cover image source: &lt;a href="https://www.pexels.com/photo/an-alert-message-on-a-cellphone-7821762" rel="noopener noreferrer"&gt;RDNE Stock project&lt;/a&gt; by RDNE&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you've been building Flutter apps for a while, you know the sinking feeling of a QA report saying: &lt;em&gt;"Push notifications stopped working on iOS."&lt;/em&gt; You check the code, and nothing has changed. You check Android, and it works perfectly.&lt;/p&gt;

&lt;p&gt;Then you log into &lt;a href="https://firebase.google.com/" rel="noopener noreferrer"&gt;Firebase&lt;/a&gt; and see it: &lt;strong&gt;The dreaded red box.&lt;/strong&gt; Your APNs certificates have expired.&lt;/p&gt;

&lt;p&gt;In this article, we'll dive into the fundamental difference between APNs Certificates and Auth Keys, and why as a seasoned developer, you should almost always choose the latter.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Contenders: .p12 vs. .p8
&lt;/h2&gt;

&lt;p&gt;For years, developers relied on &lt;strong&gt;APNs Certificates (.p12 files)&lt;/strong&gt;. While they work, they come with a high maintenance cost. Recently, Apple introduced &lt;strong&gt;APNs Authentication Keys (.p8 files)&lt;/strong&gt;, which have quickly become the industry standard.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. APNs Certificates (.p12)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Expiration:&lt;/strong&gt; They expire every &lt;strong&gt;12 months&lt;/strong&gt;. If you forget to renew them, your production notifications stop instantly.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Scope:&lt;/strong&gt; Each certificate is tied to a &lt;strong&gt;single App ID&lt;/strong&gt;. If you have 5 apps, you manage 5 certificates.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Environment:&lt;/strong&gt; You often need separate certificates for &lt;strong&gt;Development&lt;/strong&gt; (Sandbox) and &lt;strong&gt;Production&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. APNs Authentication Keys (.p8) — &lt;em&gt;Recommended&lt;/em&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Expiration:&lt;/strong&gt; They &lt;strong&gt;never expire&lt;/strong&gt;. Set it once, and forget it forever.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Scope:&lt;/strong&gt; One key works for &lt;strong&gt;all your apps&lt;/strong&gt; under a single Apple Developer account.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Environment:&lt;/strong&gt; The same key handles both &lt;strong&gt;Sandbox&lt;/strong&gt; and &lt;strong&gt;Production&lt;/strong&gt; environments seamlessly.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step-by-Step: How to Get Your APNs Credentials
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option A: The Modern Way (APNs Auth Key)
&lt;/h3&gt;

&lt;p&gt;This is the "Pro" move to ensure your notifications never die again.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Apple Developer Portal:&lt;/strong&gt; Log in and head to &lt;strong&gt;Certificates, Identifiers &amp;amp; Profiles&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Create Key:&lt;/strong&gt; Click &lt;strong&gt;Keys&lt;/strong&gt; on the left sidebar, then hit the &lt;strong&gt;(+)&lt;/strong&gt; button.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Configure:&lt;/strong&gt; Give it a name (e.g., "Firebase_Global_Key") and check the box for &lt;strong&gt;Apple Push Notifications service (APNs)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Register &amp;amp; Download:&lt;/strong&gt; Click &lt;strong&gt;Continue&lt;/strong&gt;, then &lt;strong&gt;Register&lt;/strong&gt;. Download the &lt;code&gt;.p8&lt;/code&gt; file.

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Warning:&lt;/strong&gt; &lt;em&gt;You can only download this file **ONCE&lt;/em&gt;&lt;em&gt;. Store it securely!&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Firebase Setup:&lt;/strong&gt; Go to &lt;strong&gt;Project Settings &amp;gt; Cloud Messaging&lt;/strong&gt;. Under your iOS app, upload the &lt;code&gt;.p8&lt;/code&gt; file. You’ll need your &lt;strong&gt;Key ID&lt;/strong&gt; (found on the download page) and your &lt;strong&gt;Team ID&lt;/strong&gt; (found in your Apple account membership details).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Option B: The Old Way (APNs Certificate)
&lt;/h3&gt;

&lt;p&gt;Use this only if your specific infrastructure requires it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;a href="https://developer.apple.com/" rel="noopener noreferrer"&gt;Apple Developer Portal&lt;/a&gt;:&lt;/strong&gt; Go to &lt;strong&gt;Certificates&lt;/strong&gt; and click &lt;strong&gt;(+)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Select Type:&lt;/strong&gt; Choose &lt;strong&gt;Apple Push Notification service SSL (Sandbox &amp;amp; Production)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Choose App ID:&lt;/strong&gt; Select the specific Bundle ID for your app.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;CSR Request:&lt;/strong&gt; You’ll need to upload a &lt;strong&gt;Certificate Signing Request (CSR)&lt;/strong&gt; generated from your Mac's &lt;strong&gt;Keychain Access&lt;/strong&gt; app.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Download &amp;amp; Export:&lt;/strong&gt; Once generated, download the &lt;code&gt;.cer&lt;/code&gt; file. Double-click to add it to your Keychain, then right-click and &lt;strong&gt;Export&lt;/strong&gt; it as a &lt;code&gt;.p12&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Firebase Setup:&lt;/strong&gt; Upload this &lt;code&gt;.p12&lt;/code&gt; file to Firebase under &lt;strong&gt;Cloud Messaging&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Expert Verdict
&lt;/h2&gt;

&lt;p&gt;Stop using &lt;code&gt;.p12&lt;/code&gt; certificates. The overhead of annual renewals and multi-app management is a productivity killer. Switch to a &lt;strong&gt;.p8 Auth Key&lt;/strong&gt; today to safeguard your app's communication and keep your QA team happy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Happy Fluttering!&lt;/strong&gt; 🚀&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>firebase</category>
      <category>ios</category>
      <category>apple</category>
    </item>
    <item>
      <title>Implementing App and Universal Links in Flutter</title>
      <dc:creator>George Ikwegbu Chinedu</dc:creator>
      <pubDate>Mon, 08 Apr 2024 15:06:04 +0000</pubDate>
      <link>https://dev.to/gikwegbu/implementing-app-and-universal-links-in-flutter-3j8k</link>
      <guid>https://dev.to/gikwegbu/implementing-app-and-universal-links-in-flutter-3j8k</guid>
      <description>&lt;p&gt;It's no news that, come &lt;code&gt;August 25, 2025&lt;/code&gt;, &lt;code&gt;Firebase&lt;/code&gt; will no longer support &lt;code&gt;dynamic links&lt;/code&gt; for Flutter applications. To that effect, we will need to handle the app link (for Android) and the universal link (for iOS) ourselves. And in this article, we will be doing so with the help of onGenerateRoute.&lt;/p&gt;

&lt;h3&gt;
  
  
  Table of Content
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🎉 Introduction
&lt;/li&gt;
&lt;li&gt;✨ AppLink Overview
&lt;/li&gt;
&lt;li&gt;⚙️  Updating AndroidManifest.xml
&lt;/li&gt;
&lt;li&gt;🍦 Creating SHA256
&lt;/li&gt;
&lt;li&gt;💪 Creating &amp;amp; Hosting assetlinks.json file
&lt;/li&gt;
&lt;li&gt;🧪 Testing and Confirming assetlinks.json
&lt;/li&gt;
&lt;li&gt;🚀 Deploy app to PlayStore internal testing
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ Confirm the link is auto verified in the device hardware app settings&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🌐 Universal Link Overview&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;👮🏾‍♀️ Adding capability&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;📡 Creating &amp;amp; Hosting AASA file&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔌 Confirming Our AASA file&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;⚒️ Testing Via cmd xcrun &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;⛓️ OnGenerateRoute Setup and Routing to a dynamic page with data &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🎉 Introduction &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Deep or dynamic linking is a great feature that Flutter offers in its application to enable a continuance in data consumption from a website to a mobile application. &lt;/p&gt;

&lt;p&gt;For instance, say you have an e-commerce website with products to be sold or an online campaign website where users need to vote or be voted for. A user could share a link to that product/profile with another user, who then clicks on the link to be led to the website to continue their business. In a situation where you have both a website and a mobile app and you want the users to still continue their business through the mobile interface, that is where a deep or dynamic link comes in. &lt;/p&gt;

&lt;p&gt;NB: In situations where the shared user does not have the app installed, they'd be redirected to their respective device's stores (Play or Apple Store) to download and continue with the application.&lt;/p&gt;

&lt;p&gt;PS: In this article, we will be handling the &lt;code&gt;applink&lt;/code&gt; and &lt;code&gt;universal link&lt;/code&gt; for Android and iOS, respectively.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✨ AppLink Overview &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;We will be following the official &lt;a href="https://developer.android.com/training/app-links#android-app-links" rel="noopener noreferrer"&gt;Android Guidelines&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚙️ Updating AndroidManifest.xml &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Navigate to your &lt;code&gt;AndroidManifest.xml&lt;/code&gt; file and add the below intent inside your &lt;code&gt;&amp;lt;android&amp;gt;&lt;/code&gt; block.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="nl"&gt;android:&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flutter_deeplinking_enabled"&lt;/span&gt; &lt;span class="nl"&gt;android:&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="nl"&gt;android:&lt;/span&gt;&lt;span class="n"&gt;autoVerify&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="nl"&gt;android:&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"android.intent.action.VIEW"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="nl"&gt;android:&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"android.intent.category.DEFAULT"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="nl"&gt;android:&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"android.intent.category.BROWSABLE"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="nl"&gt;android:&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="nl"&gt;android:&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"http"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="nl"&gt;android:&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"gikwegbu.com"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="nl"&gt;android:&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"www.gikwegbu.com"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NB: You need `flutter_deeplinking_enabled` for it to work; that way, the route can be passed from the Android routing system into our Flutter application.

NB: Please update the `gikwegbu` host with your personal domain name.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  🍦 Creating SHA256 &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;We will need to create an &lt;code&gt;SHA256&lt;/code&gt; for our application. To do that, open your &lt;code&gt;root&lt;/code&gt; project in the &lt;code&gt;terminal&lt;/code&gt; and run the below code:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;keytool&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;keystore&lt;/span&gt; &lt;span class="o"&gt;~/.&lt;/span&gt;&lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;keystore&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="n"&gt;androiddebugkey&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;storepass&lt;/span&gt; &lt;span class="n"&gt;android&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;keypass&lt;/span&gt; &lt;span class="n"&gt;android&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then copy the &lt;code&gt;SHA256&lt;/code&gt; value that comes up.&lt;/p&gt;

&lt;h3&gt;
  
  
  💪 Creating &amp;amp; Hosting AssetLink.json file &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;You can either use the &lt;a href="https://developers.google.com/digital-asset-links/tools/generator" rel="noopener noreferrer"&gt;Statement List Generator and Tester&lt;/a&gt; link to &lt;code&gt;Generate your statement&lt;/code&gt; and in future, &lt;code&gt;Test your statement&lt;/code&gt;. Or simply update the below json file with your own details;&lt;/p&gt;

&lt;p&gt;NB: This code &lt;code&gt;MUST&lt;/code&gt; be saved in the &lt;code&gt;assetLinks.json&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"relation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"delegate_permission/common.handle_all_urls"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"android_app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"package_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.gikwegbu.app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sha256_cert_fingerprints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"PASTE YOUR OWN SHA256 HERE"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NB: Update the &lt;code&gt;package_name&lt;/code&gt; with your app's own.&lt;/p&gt;

&lt;p&gt;Congratulations if you've gotten to this place so far; you're a genius. But before we host our &lt;code&gt;json&lt;/code&gt; file, we need to do something first. I assume you already have a &lt;code&gt;PlayStore&lt;/code&gt; account and an &lt;code&gt;APP&lt;/code&gt; created for your currently built application. If you don't, please pause and do so.&lt;/p&gt;

&lt;p&gt;There is every tendency that your app already exists in the store, and you just want to include this feature, which is perfect. All I need you to do is get the &lt;code&gt;SHA256&lt;/code&gt; google Auto-created for you application in the &lt;code&gt;integrity&lt;/code&gt; section of the console and update the &lt;code&gt;sha256_cert_fingerprints&lt;/code&gt; in your &lt;code&gt;AssetLink.json&lt;/code&gt;. Remember, it's an array of &lt;code&gt;SHA256&lt;/code&gt; values, so just a &lt;code&gt;comma&lt;/code&gt; and the new &lt;code&gt;SHA256&lt;/code&gt; value from &lt;code&gt;PlayStore console&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;    &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="s"&gt;"relation"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; 
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"delegate_permission/common.handle_all_urls"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
       &lt;span class="s"&gt;"target"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"namespace"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"android_app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;"package_name"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"com.gikwegbu.app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;"sha256_cert_fingerprints"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"PASTE YOUR OWN SHA256 HERE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"PASTE GOOGLE PLAY CONSOLE SHA256 HERE TOO"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Hosting your AssetLink.json file;
&lt;/h4&gt;

&lt;p&gt;For this, you will have to reach out to the person managing the website, as we need to upload our file to a specific directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;http:&lt;/span&gt;&lt;span class="c1"&gt;//gikwegbu.com/.well-known/assetlinks.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NB: It is very important that you place &lt;code&gt;assetlinks.json&lt;/code&gt; inside the &lt;code&gt;.well-known/&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;NB: The domain must match what you provided in your &lt;code&gt;AndroidManifest.xml&lt;/code&gt; &lt;code&gt;android:host&lt;/code&gt; data as well.&lt;/p&gt;

&lt;p&gt;NB: Once it has been hosted, make sure it's accessible by visiting &lt;code&gt;http://gikwegbu.com/.well-known/assetlinks.json&lt;/code&gt;. If it's accessible, you'll see the &lt;code&gt;json&lt;/code&gt; data displayed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fciy1b36o129zaossdn1b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fciy1b36o129zaossdn1b.png" alt="Accessible assetlink.json" width="799" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🧪 Testing and Confirming AssetLink.json file &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;To test if your &lt;code&gt;assetlink.json&lt;/code&gt; will be OK, provide the required details in the &lt;a href="https://developers.google.com/digital-asset-links/tools/generator" rel="noopener noreferrer"&gt;Statement List Generator and Tester&lt;/a&gt;, then click on &lt;code&gt;Test Statement&lt;/code&gt; to confirm. &lt;/p&gt;

&lt;p&gt;This will crawl your website, to confirm the &lt;code&gt;assetlink.json&lt;/code&gt; file is present, and the &lt;code&gt;SHA256&lt;/code&gt; keys are also valid.&lt;/p&gt;

&lt;p&gt;As a bonus, try inputing the &lt;code&gt;SHA256&lt;/code&gt; you got from the &lt;code&gt;Google Play Console&lt;/code&gt;, to confirm it works too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbcnmnqumpg9jrfvqml7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbcnmnqumpg9jrfvqml7.png" alt="Statement List Generator" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🚀 Deploy app to PlayStore internal testing &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Based on my earlier assumption that you just created the app on your &lt;code&gt;Google Console&lt;/code&gt;, we would be deploying to &lt;code&gt;internal testing&lt;/code&gt;, just so we could test out our &lt;code&gt;deep links&lt;/code&gt; before releasing them to the actual store. &lt;/p&gt;

&lt;p&gt;NB: This article won't cover how to build and upload your &lt;code&gt;appbundle&lt;/code&gt; to the store.&lt;/p&gt;

&lt;p&gt;PS: If you have already released your app, no problem, just continue with the next step. &lt;/p&gt;

&lt;p&gt;On your &lt;code&gt;Google Play Console&lt;/code&gt;, go to the &lt;code&gt;Deep Links&lt;/code&gt; on &lt;code&gt;Side Navigation&lt;/code&gt;, If everything we've done so far has worked out well, you should get a pretty dashboard like below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd6lw7v06byyonb7lcdxm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd6lw7v06byyonb7lcdxm.png" alt="Deep Link Get started" width="800" height="382"&gt;&lt;/a&gt;&lt;br&gt;
Getting Started&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4k0if3yw1ua5hkj0dg6t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4k0if3yw1ua5hkj0dg6t.png" alt="Complete File upload" width="800" height="1233"&gt;&lt;/a&gt;&lt;br&gt;
Successful Upload&lt;/p&gt;

&lt;p&gt;NB: If there's still an issue, please reach out to me, and I'll be glad to help out.&lt;/p&gt;

&lt;p&gt;NB: Once you have uploaded the &lt;code&gt;appbundle&lt;/code&gt; to your internal testing, wait for a while (say an hour; honestly, I don't know how long it takes for Google to verify your assetlink and web domains) before installing it.&lt;/p&gt;

&lt;p&gt;NB: When you have installed it on your device, go to your &lt;code&gt;flutter app's&lt;/code&gt; details, select &lt;code&gt;Set as default&lt;/code&gt;, &amp;gt; &lt;code&gt;Supported web addresses&lt;/code&gt; and see if the &lt;code&gt;host&lt;/code&gt; you provided in the &lt;code&gt;AndroidManifest.xml&lt;/code&gt; is/are present there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxzqju82vahguvvzevndq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxzqju82vahguvvzevndq.png" alt="Your flutter app settings" width="485" height="1080"&gt;&lt;/a&gt;&lt;br&gt;
Your flutter app settings&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F24n4ihwjio10mzeq3j79.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F24n4ihwjio10mzeq3j79.png" alt="Set as default" width="485" height="1080"&gt;&lt;/a&gt;&lt;br&gt;
Set as default&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frxuwalm5g1v6k10zdpn6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frxuwalm5g1v6k10zdpn6.png" alt="Approved Hosts" width="485" height="1080"&gt;&lt;/a&gt;&lt;br&gt;
Approved Hosts&lt;/p&gt;
&lt;h3&gt;
  
  
  ✅ Confirm the link is auto verified in the device hardware app settings &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Because in our &lt;code&gt;AndroidManifest.xml&lt;/code&gt;, we set the &lt;code&gt;&amp;lt;intent-filter android:autoVerify="true"&amp;gt;&lt;/code&gt;, therefore, you shouldn't see the &lt;code&gt;toggle/switch&lt;/code&gt; button beside your &lt;code&gt;host&lt;/code&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frxuwalm5g1v6k10zdpn6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frxuwalm5g1v6k10zdpn6.png" alt="Approved Hosts" width="485" height="1080"&gt;&lt;/a&gt;&lt;br&gt;
Approved Hosts&lt;/p&gt;

&lt;p&gt;Whoop Whoooop!!!! We just finished Applink on our Android.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff09aqvpza9yeirlcz0mt.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff09aqvpza9yeirlcz0mt.gif" alt="Celebration Gif" width="500" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PS: Stick around to the end of the article to learn how the routing works with the &lt;code&gt;onGenerateRoute&lt;/code&gt; configs in &lt;code&gt;Flutter&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  🌐 Universal Link Overview &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;For our iOS application, we will be using a &lt;code&gt;Universal link&lt;/code&gt;, which is an exclusive type of deep link to Apple devices that uses either the &lt;code&gt;http&lt;/code&gt; or the &lt;code&gt;https&lt;/code&gt; schemes.&lt;/p&gt;
&lt;h3&gt;
  
  
  👮🏾‍♀️ Adding capability &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;In the next few steps, we will be working closely with &lt;code&gt;XCode&lt;/code&gt; via the following steps: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open your project in &lt;code&gt;XCode&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;In your &lt;code&gt;Project Navigator&lt;/code&gt;, click on the &lt;code&gt;Runner&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Click the &lt;code&gt;Runner&lt;/code&gt; in the &lt;code&gt;TARGETS&lt;/code&gt; section.&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;Signing &amp;amp; Capabilities&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Click on the &lt;code&gt;+ Capability&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;When the &lt;code&gt;Modal&lt;/code&gt; pops up, search for &lt;code&gt;Associated Domains&lt;/code&gt; and add it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1tvnhdn3assmbz350ngl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1tvnhdn3assmbz350ngl.png" alt="Getting to capabilities modal in XCode" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvfe1o062bu8hzu4j7f2r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvfe1o062bu8hzu4j7f2r.png" alt="Searching for the Associated Domain inside capabilities in XCode" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After adding the &lt;code&gt;Associated Domains&lt;/code&gt;, update the current &lt;code&gt;web credential&lt;/code&gt; &lt;code&gt;Domains&lt;/code&gt; with;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;applinks:&lt;/span&gt;&lt;span class="n"&gt;gikwegbu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;com&lt;/span&gt; &lt;span class="c1"&gt;// first input&lt;/span&gt;
&lt;span class="nl"&gt;applinks:&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;gikwegbu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;com&lt;/span&gt; &lt;span class="c1"&gt;// second input&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NB: I like adding more options so my app will be recognized when users try to access it with or without the &lt;code&gt;www&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6zqyq0no47wr5xrzx0ob.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6zqyq0no47wr5xrzx0ob.png" alt="AppLinks values" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NB: While the above is the easiest and UI approach of adding the &lt;code&gt;Runner.entitlements&lt;/code&gt; to your app, you could just create. a &lt;code&gt;Runner.entitlements&lt;/code&gt; file within your &lt;code&gt;ios/Runner/&lt;/code&gt; directory, ad add the below code manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;DOCTYPE&lt;/span&gt; &lt;span class="n"&gt;plist&lt;/span&gt; &lt;span class="n"&gt;PUBLIC&lt;/span&gt; &lt;span class="s"&gt;"-//Apple//DTD PLIST 1.0//EN"&lt;/span&gt; &lt;span class="s"&gt;"http://www.apple.com/DTDs/PropertyList-1.0.dtd"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;plist&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apple&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;developer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;associated&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;applinks:&lt;/span&gt;&lt;span class="n"&gt;gikwegbu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;com&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nl"&gt;applinks:&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;gikwegbu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;com&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plist&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NB: &lt;code&gt;gikwegbu.com&lt;/code&gt; should be your own &lt;code&gt;domain&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From our &lt;code&gt;Step 3&lt;/code&gt; above, whilst still on the &lt;code&gt;Runner&lt;/code&gt; in the &lt;code&gt;TARGET&lt;/code&gt; section:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click on the &lt;code&gt;info&lt;/code&gt; tab.&lt;/li&gt;
&lt;li&gt;Open the &lt;code&gt;URL Types&lt;/code&gt; dropdown and complete the form with below data:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;identifier&lt;/code&gt; : your app's bundle Identifier, &lt;br&gt;
&lt;code&gt;URL Schemes&lt;/code&gt;: https.&lt;/p&gt;

&lt;p&gt;NB: You can get you &lt;code&gt;bundle Identifier&lt;/code&gt; from your &lt;code&gt;Signing &amp;amp; Capabilities&lt;/code&gt; tab in &lt;code&gt;XCode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6zqyq0no47wr5xrzx0ob.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6zqyq0no47wr5xrzx0ob.png" alt="AppLinks values" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NB: For the iOS routing system to successfully parse the dynamic url back into our Flutter application, we need to add the below data into our info.plist file;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;FlutterDeepLinkingEnabled&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  📡 Creating &amp;amp; Hosting AASA file &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Open your &lt;code&gt;project root&lt;/code&gt; in the &lt;code&gt;terminal&lt;/code&gt; and run the below code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="n"&gt;touch&lt;/span&gt; &lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;association&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then copy and paste  the below code, with your own &lt;code&gt;data&lt;/code&gt;;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;   
  &lt;span class="s"&gt;"applinks"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;       
    &lt;span class="s"&gt;"apps"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;

    &lt;span class="s"&gt;"details"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;           
        &lt;span class="s"&gt;"appID"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"[TEAM_ID].[APP_BUNDLE_ID]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           
        &lt;span class="s"&gt;"paths"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"[PATH_FOR_REDIRECTION]"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NB: To get your &lt;code&gt;Team_ID&lt;/code&gt;, just open your &lt;a href="https://developer.apple.com/account/resources/certificates/list" rel="noopener noreferrer"&gt;apple dev account&lt;/a&gt;, you'd see the ID by the &lt;code&gt;top right&lt;/code&gt;, &lt;code&gt;underneath your name&lt;/code&gt; of your screen, &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"paths": [
       "/your_path", // Users will be be redirected if they visit &amp;lt;https://gikwegbu.com/your_path&amp;gt;
       "/your_path/*", // Users will be redirected if they visit  &amp;lt;https://gikwegbu.com/your_path/[shduewjakssd]&amp;gt;
       "*", // will be redirected to any link &amp;lt;https://gikwegbu.com/[wild card links]&amp;gt;
       "NOT /your_path", // Users will be be redirected  to any link other than &amp;lt;https://gikwegbu.com/your_path&amp;gt;
       "NOT /your_path/*", // Users be redirected to any link other than &amp;lt;https://gikwegbu.com/your_path/[wild card]&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;]&lt;/p&gt;

&lt;p&gt;Once you're done with the above, host the AASA file on your domain, within the &lt;code&gt;.well-known/&lt;/code&gt; directory, just like you did with your &lt;code&gt;Android&lt;/code&gt; &lt;code&gt;assetlinks.json&lt;/code&gt; file.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔌 Confirming Our AASA file&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;To confirm if the AASA file was properly hosted, once you visit the &lt;code&gt;http://gikwegbu.com/.well-known/apple-app-site-association&lt;/code&gt;, the file will automatically be downloaded onto your machine.&lt;/p&gt;

&lt;p&gt;NB: &lt;code&gt;gikwegbu&lt;/code&gt; will be your own domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚒️ Testing Via cmd xcrun &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;First make sure your &lt;code&gt;XCode&lt;/code&gt; is running your &lt;code&gt;Flutter app&lt;/code&gt;, Open your &lt;code&gt;terminal&lt;/code&gt; and run the below code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="n"&gt;xcrun&lt;/span&gt; &lt;span class="n"&gt;simctl&lt;/span&gt; &lt;span class="n"&gt;openurl&lt;/span&gt; &lt;span class="n"&gt;booted&lt;/span&gt; &lt;span class="nl"&gt;https:&lt;/span&gt;&lt;span class="c1"&gt;//gikwegbu.com/guest/dsewe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NB: Since we haven't worked on the Routing system in our Flutter app, the above command should simply launch your app.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  ⛓️ OnGenerateRoute Setup and Routing to a dynamic page with data&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;For an efficient and cleaner codebase, I like to keep all my routes in a separate file.&lt;/p&gt;

&lt;p&gt;Within your &lt;code&gt;MaterialApp&lt;/code&gt;, in your &lt;code&gt;main.dart&lt;/code&gt; file, you can add your &lt;code&gt;onGenerateRoute&lt;/code&gt; data and point it to the external &lt;code&gt;routes&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fnaibp0gt351pfr1k22.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fnaibp0gt351pfr1k22.png" alt=" " width="465" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Image of the main.dart file&lt;/p&gt;

&lt;p&gt;Here, we will create a class called &lt;code&gt;Routes&lt;/code&gt;;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Routes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;generateRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RouteSettings&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${settings.name}&lt;/span&gt;&lt;span class="s"&gt; Is the current route'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'guest'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PreInviteInfoScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;code:&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialPageRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// For screens that requires arguements to be passed&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;HomeView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;routeName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HomeView&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialPageRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HomeView&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialPageRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NB: For more context, say the  link our users clicks on is &lt;code&gt;https://www.gikwegbu.com/guest/Uhdys&lt;/code&gt;,  the &lt;code&gt;if statement&lt;/code&gt;, will check if that route has the &lt;code&gt;guest&lt;/code&gt; keyword, then extract to get the code as it a &lt;code&gt;URL PATH&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This way, when a user clicks on the link above,  only the &lt;code&gt;Uhdys&lt;/code&gt; is now being passed as the &lt;code&gt;argument&lt;/code&gt; for the next screen immediately the app launches.&lt;/p&gt;

&lt;h2&gt;
  
  
  THE END
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh0mlb4xgevdm9v61bjp0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh0mlb4xgevdm9v61bjp0.gif" alt="Congratulations" width="480" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So with the above sets of instructions and examples, you should be able to get the Applink and Universal link ready for your FLutter application. CONGRATULATIONS AND GOOD LUCK.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>beginners</category>
      <category>deeplink</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Custom Flutter GroupListView Widget</title>
      <dc:creator>George Ikwegbu Chinedu</dc:creator>
      <pubDate>Mon, 05 Feb 2024 09:08:50 +0000</pubDate>
      <link>https://dev.to/gikwegbu/custom-flutter-grouplistview-widget-oad</link>
      <guid>https://dev.to/gikwegbu/custom-flutter-grouplistview-widget-oad</guid>
      <description>&lt;h3&gt;
  
  
  Table of Content
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🎉 Introduction
&lt;/li&gt;
&lt;li&gt;✨ Items JSON
&lt;/li&gt;
&lt;li&gt; ⚙️ Modeling our Data
&lt;/li&gt;
&lt;li&gt;🍦 Implementation
&lt;/li&gt;
&lt;li&gt;💪 Full Code
&lt;/li&gt;
&lt;li&gt;💙 Summary
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🎉 Introduction &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;You may need to arrange items in an orderly manner at some point. These objects might be anything, employing a specific property in their object, from a list of books to questions.&lt;/p&gt;

&lt;p&gt;NB: Check out my previous article, in which I explain how to create a form in Flutter dynamically. I used this article to group the questions according to their order.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✨ Items JSON &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;I would be constructing a basic JSON data set as our source data in order to keep things simple. Although this will be relatively little data, you can reuse the function in your larger dataset after you understand how it operates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Continent and Country Json data&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;itemsJsonData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 

&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Africa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Nigeria"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Asia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Cambodia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"NorthAmerica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Canada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"NorthAmerica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"SouthAmerica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Brazil"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Europe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Austria"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Africa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Burundi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Asia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Azerbaijan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"NorthAmerica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Belize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"NorthAmerica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Antigua and Barbuda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"SouthAmerica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Argentina"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Europe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Andorra"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

&lt;span class="p"&gt;];&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NB: Pay close attention to the continent and country 
dataset and it's ordering and how it is an `int`..

NB: Request the server sends the `order` as an `int` 
value. Else, you might have to map over the keys and turn 
them to `int`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  ⚙️ Modeling our Data 
&lt;/h3&gt;

&lt;p&gt;For simplicity sake, we would be creating a model, which will enable us interact with the data efficiently.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContCountryModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;continent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;continent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;});&lt;/span&gt;

   &lt;span class="kd"&gt;factory&lt;/span&gt; &lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nl"&gt;continent:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nl"&gt;country:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nl"&gt;order:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;toJson&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;continent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fromList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🍦 Implementation &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;NB: We won't be covering the network calls; as I assume, you already have the data list in your system and just want to create the grouped list&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// We will be needing the `groupBy` found in the `collection` package to first group our items.&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:collection/collection.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;



&lt;span class="c1"&gt;// Placed within the State of your stateful widget&lt;/span&gt;
&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_itemList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="kt"&gt;Map&lt;/span&gt; &lt;span class="nf"&gt;groupItemsByOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// When the above function runs, it'll arrange all the items by their `order`. But since the `order` is a `String` datatype, it won't be mathematically correct, e.g; `10` will come before `2`.&lt;/span&gt;


&lt;span class="c1"&gt;// Placed within state builder:&lt;/span&gt;

&lt;span class="kt"&gt;Map&lt;/span&gt; &lt;span class="n"&gt;groupedItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;groupItemsByOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_itemList&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// NB: You can either load the `_itemList` in the `initState` or use any `state management` system of your choice to get the data.&lt;/span&gt;


&lt;span class="c1"&gt;// Here we will be using a `ListView.builder` within a `ListView.builder` to get our `Continents` and the `Countries` within it in a grouped fashion&lt;/span&gt;


&lt;span class="n"&gt;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;itemCount:&lt;/span&gt; &lt;span class="n"&gt;groupedItems&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;itemBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sortedKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;groupedItems&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
   &lt;span class="c1"&gt;// Since the groupedItems is a Map,we can use the .keys, &lt;/span&gt;
      &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="n"&gt;turn&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;into&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt; 
      &lt;span class="n"&gt;sorting&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;

    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sortedKeys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;elementAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Recall that groupedItems returns a Map of data grouped &lt;/span&gt;
       &lt;span class="n"&gt;by&lt;/span&gt; &lt;span class="s"&gt;'order'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hence&lt;/span&gt; &lt;span class="n"&gt;using&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;above&lt;/span&gt; &lt;span class="n"&gt;listView&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// we fetch the key that matches the index, which inturn &lt;/span&gt;
       &lt;span class="n"&gt;becomes&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="s"&gt;'order'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

    &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;itemsInCategory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="n"&gt;groupedItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

     &lt;span class="c1"&gt;// With the help of the retrieved `order` , we now get &lt;/span&gt;
        &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;which&lt;/span&gt; &lt;span class="n"&gt;we&lt;/span&gt; &lt;span class="n"&gt;would&lt;/span&gt; 
        &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="n"&gt;ListView&lt;/span&gt;

     &lt;span class="c1"&gt;// Return a widget representing the category and its &lt;/span&gt;
        &lt;span class="n"&gt;items&lt;/span&gt;

     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                 &lt;span class="n"&gt;Padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                   &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;top:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                   &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                         &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${itemsInCategory.first.continent}&lt;/span&gt;&lt;span class="s"&gt;:"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                   &lt;span class="p"&gt;),&lt;/span&gt;
                 &lt;span class="n"&gt;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                   &lt;span class="nl"&gt;shrinkWrap:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="nl"&gt;physics:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ClampingScrollPhysics&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                   &lt;span class="nl"&gt;itemCount:&lt;/span&gt; &lt;span class="n"&gt;itemsInCategory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="nl"&gt;itemBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; 
                       &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                       &lt;span class="n"&gt;ContCountryModel&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
                      &lt;span class="n"&gt;itemsInCategory&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                          &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_originalListIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_itemList&lt;/span&gt;
                              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;indexWhere&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                          &lt;span class="c1"&gt;// Return a widget representing the item&lt;/span&gt;
                          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;symmetric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                              &lt;span class="nl"&gt;vertical:&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="p"&gt;),&lt;/span&gt;
                            &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${each.country}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
     &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
   &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  💪 Full Code &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Here is the full code implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/material.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:collection/collection.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;


&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyApp&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="s"&gt;'GrouupedList Demo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;debugShowCheckedModeBanner:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;theme:&lt;/span&gt; &lt;span class="n"&gt;ThemeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;colorScheme:&lt;/span&gt; &lt;span class="n"&gt;ColorScheme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seedColor:&lt;/span&gt; &lt;span class="n"&gt;Colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deepPurple&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;useMaterial3:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyHomePage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="s"&gt;'Flutter Grouped List'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;



&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyHomePage&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyHomePage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyHomePage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_MyHomePageState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_MyHomePageState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyHomePage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="c1"&gt;// Placed within the State of your stateful widget&lt;/span&gt;
  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_itemList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="kt"&gt;Map&lt;/span&gt; &lt;span class="n"&gt;groupItemsByOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// When the above function runs, it'll arrange all the items by their `order`. But since the `order` is a `String` datatype, it won't be mathematically correct, e.g; `10` will come before `2`.&lt;/span&gt;

   &lt;span class="nd"&gt;@override&lt;/span&gt;
   &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_itemList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;itemsJsonData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;Map&lt;/span&gt; &lt;span class="n"&gt;groupedItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;groupItemsByOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_itemList&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;appBar:&lt;/span&gt; &lt;span class="n"&gt;AppBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;backgroundColor:&lt;/span&gt; &lt;span class="n"&gt;Theme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;colorScheme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;inversePrimary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;infinity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;mainAxisAlignment:&lt;/span&gt; &lt;span class="n"&gt;MainAxisAlignment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;Expanded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;itemCount:&lt;/span&gt; &lt;span class="n"&gt;groupedItems&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nl"&gt;itemBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

                  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sortedKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;groupedItems&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                  &lt;span class="c1"&gt;// Since the groupedItems is a Map,we can use the .keys, to get all the keys, then turn it into a list before sorting it&lt;/span&gt;

                  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sortedKeys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;elementAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                  &lt;span class="c1"&gt;// Recall that groupedItems returns a Map of data grouped by 'order', hence using the above listView index,&lt;/span&gt;
                  &lt;span class="c1"&gt;// we fetch the key that matches the index, which inturn becomes the active 'order' in the current loop.&lt;/span&gt;

                  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;itemsInCategory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
                      &lt;span class="n"&gt;groupedItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                  &lt;span class="c1"&gt;// With the help of the retrieved `order` , we now get the items in the category data, with which we would use to create the second ListView&lt;/span&gt;

                  &lt;span class="c1"&gt;// Return a widget representing the category and its items&lt;/span&gt;
                  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                      &lt;span class="n"&gt;Padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;top:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                              &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${itemsInCategory.first.continent}&lt;/span&gt;&lt;span class="s"&gt;:"&lt;/span&gt;
                            &lt;span class="p"&gt;),&lt;/span&gt;
                      &lt;span class="p"&gt;),&lt;/span&gt;
                      &lt;span class="n"&gt;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="nl"&gt;shrinkWrap:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="nl"&gt;physics:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ClampingScrollPhysics&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                        &lt;span class="nl"&gt;itemCount:&lt;/span&gt; &lt;span class="n"&gt;itemsInCategory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="nl"&gt;itemBuilder:&lt;/span&gt;
                            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                          &lt;span class="n"&gt;ContCountryModel&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;itemsInCategory&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                          &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_originalListIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_itemList&lt;/span&gt;
                              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;indexWhere&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                          &lt;span class="c1"&gt;// Return a widget representing the item&lt;/span&gt;
                          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;symmetric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                              &lt;span class="nl"&gt;vertical:&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="p"&gt;),&lt;/span&gt;
                            &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${each.country}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                          &lt;span class="p"&gt;);&lt;/span&gt;
                        &lt;span class="p"&gt;},&lt;/span&gt;
                      &lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;],&lt;/span&gt;
                  &lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// ListView Ends here&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;// Body ends here&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContCountryModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;continent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;continent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;});&lt;/span&gt;

   &lt;span class="kd"&gt;factory&lt;/span&gt; &lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nl"&gt;continent:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nl"&gt;country:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nl"&gt;order:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;toJson&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;continent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fromList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ContCountryModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Continent and Country Json data&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;itemsJsonData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 

&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Africa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Nigeria"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Asia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Cambodia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"NorthAmerica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Canada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"NorthAmerica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"SouthAmerica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Brazil"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Europe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Austria"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Africa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Burundi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Asia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Azerbaijan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"NorthAmerica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Belize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"NorthAmerica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Antigua and Barbuda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"SouthAmerica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Argentina"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"continent"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Europe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"country"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Andorra"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"order"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

&lt;span class="p"&gt;];&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  💙 Summary &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;In this article, we were able to&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;create a &lt;code&gt;model&lt;/code&gt; class for our item that also houses a &lt;code&gt;.fromList&lt;/code&gt; method that enables you to easily get a list of items from a &lt;code&gt;dynamic&lt;/code&gt; list and the &lt;code&gt;.fromJson&lt;/code&gt; also to easily map our json data onto our model.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Provide a dummy list for the item, and lastly, &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Implement a &lt;code&gt;ListView&lt;/code&gt; within a &lt;code&gt;ListView&lt;/code&gt;, which would represent the sorted items while having it's continent as a  Header placed above each section of the list..&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NB: Once you understand how the above was achieved, you can create multiple UI using the same approach.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>list</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Flutter Apple Auth Implementation</title>
      <dc:creator>George Ikwegbu Chinedu</dc:creator>
      <pubDate>Tue, 16 Jan 2024 09:14:25 +0000</pubDate>
      <link>https://dev.to/gikwegbu/flutter-apple-auth-implementation-1pa2</link>
      <guid>https://dev.to/gikwegbu/flutter-apple-auth-implementation-1pa2</guid>
      <description>&lt;p&gt;NB: This article is not for a total beginner, as you need a basic understanding of how Flutter works and API consumption to fully comprehend this article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Table of Content
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✨ Introduction
&lt;/li&gt;
&lt;li&gt;💪 Flutter Auth Packages
&lt;/li&gt;
&lt;li&gt;🍏 Apple accepted Data Flow
&lt;/li&gt;
&lt;li&gt;😬 Edge Cases
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ✨ Introduction &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Apple Sign-In is a feature that enables users of your application to either sign up or sign in to your application using their iCloud account across all of your platforms while utilizing two-factor authentication. In this article, we will be looking at how to use the Flutter package to complete this feature on a mobile phone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr31cbo5jg54nztc2zkgd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr31cbo5jg54nztc2zkgd.png" alt="Sign in with Apple" width="748" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sign in with Apple flow: From Apple&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For full information about this &lt;code&gt;sign in with Apple&lt;/code&gt;, please make reference to this site: &lt;a href="https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple" rel="noopener noreferrer"&gt;Authenticating users with Sign in with Apple&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  💪 Flutter Auth Packages &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;To follow up with this article, we will be using the &lt;a href="https://pub.dev/packages/sign_in_with_apple" rel="noopener noreferrer"&gt;sign_in_with_apple&lt;/a&gt; package. To avoid repetitions, you can follow the instructions on this package page.&lt;/p&gt;

&lt;p&gt;In summary, select &lt;code&gt;Sign In with Apple Capability&lt;/code&gt;.&lt;br&gt;
NB: You can achieve this by simply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Going to your XCode, &lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;Signing &amp;amp; Capabilities&lt;/code&gt; tab, &lt;/li&gt;
&lt;li&gt;Click on the &lt;code&gt;Capability&lt;/code&gt; beneath, a modal pops up. &lt;/li&gt;
&lt;li&gt;Either scroll down or just search for &lt;code&gt;sign&lt;/code&gt;, you should see the option, &lt;/li&gt;
&lt;li&gt;Select to enable it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6uiungv3q9vl1cido8ta.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6uiungv3q9vl1cido8ta.png" alt="Xcode image" width="800" height="611"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  🍏 Apple accepted Data Flow &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Once a user has authenticated and either accepted to show or hide their email, they should be either registered or logged into your application (depending on what feature you're using it for) and not redirected to complete anything else, as that will make the review team flag your app during testing.&lt;/p&gt;

&lt;p&gt;Best advice: make your signup endpoint either flexible or have a different endpoint for social OAuths. That way, you get to prompt the user to update their profiles once they're already logged into the system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;SignInWithAppleButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;credential&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SignInWithApple&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAppleIDCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;scopes:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;AppleIDAuthorizationScopes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;AppleIDAuthorizationScopes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fullName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Now send the credential (especially `credential.authorizationCode`) to your server to create a session&lt;/span&gt;
    &lt;span class="c1"&gt;// after they have been validated with Apple (see `Integration` section for more information on how to do this)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3lyc0nqwxmwpomzl9p9t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3lyc0nqwxmwpomzl9p9t.png" alt="Apple Agreement" width="584" height="1048"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zuzcoihi4fhy8gwbpg3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zuzcoihi4fhy8gwbpg3.png" alt="Auth Selection" width="584" height="1048"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Images from &lt;a href="https://pub.dev/packages/sign_in_with_apple" rel="noopener noreferrer"&gt;Sign_in_with_apple&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  😬 Edge Cases &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Apple's authentication is so security tight, that you as the developer will only get the user's &lt;code&gt;email&lt;/code&gt; just once, and which is the initial time they used the apple service to sign into your application. If the log out of your application and tried to log back in, there's a tendency of it not working, as you might be expecting their email to continue.&lt;/p&gt;

&lt;p&gt;To circumvent that, I'd suggest that you first check if the email is returned from the above code, if yes then continue with your normal implementation, else, send the &lt;code&gt;identityToken&lt;/code&gt;, which is a value returned from the &lt;code&gt;apple&lt;/code&gt; api call to your server. Then your server gets to decode that &lt;code&gt;jwt_token&lt;/code&gt;, gets the email address and checks if user exist in the system.&lt;/p&gt;

&lt;p&gt;PS: You can actually decode the &lt;code&gt;jwt_tpken&lt;/code&gt; from you flutter side first and still re-use your supposed api.&lt;/p&gt;

&lt;p&gt;This might be a security flaw, what do you think?&lt;/p&gt;

</description>
    </item>
    <item>
      <title>flutter iOS app is stuck on launchScreen when installed on real device 🚀🚀🚀🚀</title>
      <dc:creator>George Ikwegbu Chinedu</dc:creator>
      <pubDate>Tue, 05 Dec 2023 20:09:22 +0000</pubDate>
      <link>https://dev.to/gikwegbu/flutter-ios-app-is-stuck-on-launchscreen-when-installed-on-real-device-3ba</link>
      <guid>https://dev.to/gikwegbu/flutter-ios-app-is-stuck-on-launchscreen-when-installed-on-real-device-3ba</guid>
      <description>&lt;p&gt;NB: This is going to be a really short article 🥹🥹. &lt;/p&gt;

&lt;p&gt;While working on my application with my team, we noticed our app was stuck on the splash screen, though this wasn't happening across all &lt;code&gt;devices&lt;/code&gt;, as some ios &lt;code&gt;devices&lt;/code&gt; went past the splash screen while some were simply stuck.&lt;br&gt;
&amp;nbsp;&lt;br&gt;
This even made apple to reject our submission on the basis of &lt;code&gt;Performance - App Completeness&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;To keep this short, make sure that all your functions in your &lt;code&gt;main.dart&lt;/code&gt; is working well. In my own case, the culprit was my &lt;code&gt;setupNotification()&lt;/code&gt; where I handled my &lt;code&gt;pushNotification&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This required me to get the &lt;code&gt;firebase token&lt;/code&gt; and also subscribe the device to a topic.&lt;/p&gt;

&lt;p&gt;Well, this was where my headache started. I had gone forward to subscribe the device to a &lt;code&gt;topic&lt;/code&gt;, which was  causing the problem. The painful part was that I didn't get log on my &lt;code&gt;IDE&lt;/code&gt;. Luckily for me I was able to see the &lt;code&gt;exception&lt;/code&gt; from my &lt;code&gt;firebase crashlytics&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Apparently, I needed to request for the &lt;code&gt;APNSToken&lt;/code&gt; for &lt;code&gt;iOS&lt;/code&gt; before I can subscribe to the topic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;FirebaseMessaging&lt;/span&gt; &lt;span class="n"&gt;_firebaseMessaging&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FirebaseMessaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Platform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isIOS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;apnsToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_firebaseMessaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAPNSToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apnsToken&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_firebaseMessaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribeToTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'notificationChannel'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;on&lt;/span&gt; &lt;span class="n"&gt;FirebaseException&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"George here is the error: &lt;/span&gt;&lt;span class="si"&gt;$e&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delayed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seconds:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;apnsToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_firebaseMessaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAPNSToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apnsToken&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_firebaseMessaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribeToTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'notificationChannel'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;on&lt;/span&gt; &lt;span class="n"&gt;FirebaseException&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"George here is the error: &lt;/span&gt;&lt;span class="si"&gt;$e&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_firebaseMessaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribeToTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'notificationChannel'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;on&lt;/span&gt; &lt;span class="n"&gt;FirebaseException&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"George here is the error: &lt;/span&gt;&lt;span class="si"&gt;$e&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, the error was corrected.&lt;/p&gt;

&lt;p&gt;NB: Just trace the functions you have running in your main that might be throwing an error.&lt;/p&gt;

&lt;p&gt;Thanks...&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Integrating Zendesk with Flutter</title>
      <dc:creator>George Ikwegbu Chinedu</dc:creator>
      <pubDate>Mon, 22 Aug 2022 20:02:04 +0000</pubDate>
      <link>https://dev.to/gikwegbu/integrating-zendesk-with-flutter-2jol</link>
      <guid>https://dev.to/gikwegbu/integrating-zendesk-with-flutter-2jol</guid>
      <description>&lt;p&gt;Image Credit: &lt;a href="https://www.pexels.com" rel="noopener noreferrer"&gt;Pexels&lt;/a&gt; by &lt;a href="https://www.pexels.com/@cottonbro/" rel="noopener noreferrer"&gt;Cottonbro&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Content
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🕹 Introduction
&lt;/li&gt;
&lt;li&gt;🎥 Tips
&lt;/li&gt;
&lt;li&gt;🧰 Zendesk Flutter Package
&lt;/li&gt;
&lt;li&gt;🧰 implementation
&lt;/li&gt;
&lt;li&gt;🎙 Setting up Channel Message
&lt;/li&gt;
&lt;li&gt;📲 Codebase
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🕹 Introduction &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.zendesk.com/" rel="noopener noreferrer"&gt;Zendesk&lt;/a&gt; is an American company headquartered in San Francisco, California. It provides software-as-a-service (Saas) products related to customer support, sales, and other customer communications. &lt;/p&gt;

&lt;p&gt;This Saas, provides a ticketing platform for users, and companies at large. In this article, you'll learn how to integrate zendesk into your flutter application with ease, setup and configure your zendesk dashboard for the utmost experience. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NB: This article covers both the android and iOS Flutter platform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  🎥 Tips &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Before we continue, there are somethings I'd love to get out of the way, &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;You should be familiar with Flutter and how to add Packages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Already created a zendesk admin account or have access to it. If you don't (probably as the developer), do well to reach out to the person managing the zendesk platform, share this article or explain the information you'll be needing from them.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  🧰 Zendesk Flutter Package &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;There are lots of zendesk packages on the pub.dev platform, but I found one to be very detailed and worked well for me. Click on &lt;a href="https://pub.dev/packages/zendesk_messaging" rel="noopener noreferrer"&gt;zendesk_messaging&lt;/a&gt;, copy the current version and add to your pubspec.yaml file.&lt;/p&gt;
&lt;h3&gt;
  
  
  🎥 Implementation &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Once you have added the package to your pubspec.yaml, run the below code in the terminal of your project's root.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;flutter&lt;/span&gt; &lt;span class="n"&gt;pub&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other not to repeat the implementations, do follow through with what you see on the &lt;a href="https://pub.dev/packages/zendesk_messaging" rel="noopener noreferrer"&gt;zendesk_messaging&lt;/a&gt; package screen. I'd only walk you through where to get the necessary information.&lt;/p&gt;

&lt;h3&gt;
  
  
  🎙 Setting up Channel Message &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;On your zendesk admin dashboard, with side navigation bar, go to the Channel -&amp;gt; Messaging and Social, select the message tab. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn4uc33fur1w64t2yy8r6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn4uc33fur1w64t2yy8r6.png" alt="Channel Message" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Incase no messaging channel has been added yet, you'd have to click on the 'Add Channel' dropdown button, to select your options, which in our case will be the 'Android and iOS'&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv754d6oguqfj8lti4xyk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv754d6oguqfj8lti4xyk.png" alt="Channel Message - Options" width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Upon selection of either the Android or iOS channel, you'll be walked through on the settings, there you'll find your "channel key", please copy this out and store in your code. &lt;/p&gt;

&lt;p&gt;NB: It's platform specific, meaning both the Android and iOS have different channel keys.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo82347af6gn8485g84dc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo82347af6gn8485g84dc.png" alt="Channel Message - Configurations" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the Above Image, you'd notice six (6) tabs; &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. The Basic, which is essentially where you see the name 
   of the channel you're currently working on.

2. The Style, here you set the color of the zendesk on 
   either platform you're working on, you could provide 
   your app's color, so it'll still match your beautiful 
   UI.

3. The Response, you get to configure how to respond to 
   messages, add bot privileges etc...

4. The Notification, well it's like adding Firebase Cloud 
   Messaging, for push notifications in your mobile app.

5. The Authentication, here, you'll be redirected to go 
   setup your "jwt", a short form of "Javascript Web 
   Token", which plays a vital role, in recognising  and 
   retaining users once they are chatting with you (The 
   help center)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9d31v25my39qcmq0amxp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9d31v25my39qcmq0amxp.png" alt="Channel Message - Channel Authentication" width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  📲  Codebase &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Like I said previously, if you (as a developer), don't have access to the zendesk dashboard, you can request for the above information from the person managing the dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initialising Zendesk in your applicaition.
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// paste the android and ios channel keys you earlier copied out here.&lt;/span&gt;

&lt;span class="c1"&gt;// These keys, should be placed in a .env file, and added to the .gitignore file, so you don't check it into any version control system.&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;androidChannelKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;iosChannelKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;ZendeskMessaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;androidChannelKey:&lt;/span&gt; &lt;span class="n"&gt;androidChannelKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;iosChannelKey:&lt;/span&gt; &lt;span class="n"&gt;iosChannelKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Showing the Zendesk
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;ZendeskMessaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;show&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NB: The above method could be added to any GestureDetector of your choice, say onTap, of a button, to pop up the zendesk platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authenticating  Users
&lt;/h3&gt;

&lt;p&gt;This is optional, but necessary if you want to keep track of the user chat across platforms, meaning, if user closes and opens the application again, the chat history will be there. &lt;/p&gt;

&lt;p&gt;PS: The Zendesk must be initialised before you authenticate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Remember the 'jwt' from the Authentication tab on your zendesk dashboard, replace the "YOUR_JWT" with your actual 'jwt'&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;ZendeskLoginResponse&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ZendeskMessaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loginUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;jwt:&lt;/span&gt; &lt;span class="s"&gt;"YOUR_JWT"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ZendeskMessaging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;logoutUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>flutter</category>
      <category>zendesk</category>
      <category>beginners</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Flutter Paystack All Options Implementation</title>
      <dc:creator>George Ikwegbu Chinedu</dc:creator>
      <pubDate>Thu, 21 Jul 2022 08:51:00 +0000</pubDate>
      <link>https://dev.to/gikwegbu/flutter-paystack-all-options-implementation-1p01</link>
      <guid>https://dev.to/gikwegbu/flutter-paystack-all-options-implementation-1p01</guid>
      <description>&lt;h2&gt;
  
  
  Table of Content
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🕹 Introduction
&lt;/li&gt;
&lt;li&gt;📡 Required Paystack Details and Endpoints
&lt;/li&gt;
&lt;li&gt;🖲 Installing WebView Package
&lt;/li&gt;
&lt;li&gt;🛢 Making API calls in Flutter
&lt;/li&gt;
&lt;li&gt;📲 Displaying Paystack pop-up in webView
&lt;/li&gt;
&lt;li&gt;💾 Listening For Navigation url
&lt;/li&gt;
&lt;li&gt;💙 Important Notes for Release APK
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🕹 Introduction &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;As at the time of writing this article, paystack integration for android and mobile SDK, only supports card payments and bank (with just Zenith as the option).&lt;/p&gt;

&lt;p&gt;In this article, you will learn how to include other payment options in your flutter application. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PS: I'm assuming you are familiar with flutter and 
paystack with all the necessary keys and how to get them 
from your paystack dashboard.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Before we continue, please take a quick look at this article from &lt;a href="https://paystack.com/docs/guides/using_the_paystack_checkout_in_a_mobile_webview/" rel="noopener noreferrer"&gt;Paystack&lt;/a&gt;, that's where I got ideas from 😋. If you want, you could use that article, or continue reading.&lt;/p&gt;

&lt;h3&gt;
  
  
  📡 Required Paystack Details and Endpoints&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;You will need your paystack secret_key (use the test key for testing and live key for prod) as your Bearer token in your Authorization header while making a POST request to the &lt;a href="https://api.paystack.co/transaction/initialize" rel="noopener noreferrer"&gt;paystack initialize api&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Check the below images for better understanding.&lt;/p&gt;

&lt;h4&gt;
  
  
  Setting your authorization header with secret_key
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6i6x7jlhyzc2vlva4kfi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6i6x7jlhyzc2vlva4kfi.png" alt="Postman Authorization Header Bearer token" width="800" height="745"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  POST body details
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fehhfh408466nodgtzxbn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fehhfh408466nodgtzxbn.png" alt="POST body details" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;email: This is the payers email.

amount: This is the amount to be paid, please multiply by 
        100, to remove the kobo value

reference: This should be randomly generated as it should 
           be a unique value for each payment.

callback_url: This is the most important part. If you 
              already have a callback url, setup from your 
              paystack dashboard, you don't need this 
              parameter. If you still go ahead to include 
              it, it'll override that of the dashboard. 
              This callback_url is what we will be 
              listening for from our webview.

cancel_action: This will be used to listen for when the 
               user wants to cancel payment, we then pop 
               the webview screen.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h4&gt;
  
  
  Response from paystack API
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuxfxzsuhjpsd6wc5hgfm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuxfxzsuhjpsd6wc5hgfm.png" alt="Paystack Response" width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;authorization_url: This is the most important data we will 
                   be needing for our webview, so hold on 
                   to it 😎.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  🖲 Installing WebView Package&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;To install this package, please visit this &lt;a href="https://dartlang-pub.appspot.com/packages/webview_flutter" rel="noopener noreferrer"&gt;link&lt;/a&gt;, read through the setup and install it.&lt;/p&gt;
&lt;h3&gt;
  
  
  🛢 Making API calls in Flutter &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;This approach depends on which architecture you're using to build your application, at the end, it's all the same response you will receive. In this article, i'll be keeping it really simple 🥹.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer sk_test_039***************&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.paystack.co/transaction/initialize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;g.ikwegbu@gmail.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reference&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;randomDetails&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;callback_url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/gikwegbu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;metadata&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cancel_action&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.google.com/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StreamedResponse&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bytesToString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reasonPhrase&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;br&gt;
PS: I personally love using the &lt;a href="https://dartlang-pub.appspot.com/packages/dio" rel="noopener noreferrer"&gt;flutter dio package&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Retrieve the authorization_url from the response and store 
it in a global variable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  📲 Displaying Paystack pop-up in webView &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Ok, let's be on the same page here 🙃. We are building an application that allows users make payment for an item via paystack. &lt;/p&gt;
&lt;h5&gt;
  
  
  🏳️ The Data Flow:
&lt;/h5&gt;

&lt;p&gt;when the user clicks on the PAY button, the paystack pops-up, user selects whichever option that's suitable, and proceed. Once the payment is successful, we will be redirected to a callback_url. Once this happens, we will have to be listening for that callback_url, which when detected, allows us to close the webview widget, and then continue with sending our reference code to our server to complete checking out of the PAID item.&lt;/p&gt;
&lt;h5&gt;
  
  
  🏳️ Implementation:
&lt;/h5&gt;

&lt;p&gt;To make the process seamless, i'd be using a popup dialog, set it to fullscreen to overlay our current page, add the webview widget.&lt;/p&gt;

&lt;p&gt;Below is a code snippet that will help you understand more:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="c1"&gt;// This is called when the PAY button is clicked.&lt;/span&gt;
&lt;span class="nf"&gt;_showPaystack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ChargedFromMyApp_${DateTime.now().millisecondsSinceEpoch}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;Map&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Removing the kobo by multiplying with 100&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;double&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;${_amount * 100}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toInt&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reference&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;callback_url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/gikwegbu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;metadata&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cancel_action&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.google.com/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="c1"&gt;// This awaits the [authorization_url](#authUrl). NB: I'm using the MVVM architecture in my live code, but it's still the same process of retrieving the authURL.&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;authUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;_profileCtrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paystackWebViewChargeCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// only pull-up the dialog box when we get the authURL&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authUrl&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;showGeneralDialog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;barrierDismissible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;barrierLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
              &lt;span class="nx"&gt;MaterialLocalizations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;modalBarrierDismissLabel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;barrierColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;black45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;transitionDuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;milliseconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;pageBuilder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BuildContext&lt;/span&gt; &lt;span class="nx"&gt;buildContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Animation&lt;/span&gt; &lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nx"&gt;Animation&lt;/span&gt; &lt;span class="nx"&gt;secondaryAnimation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="na"&gt;child&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MediaQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="c1"&gt;// height: MediaQuery.of(context).size.height - 80,&lt;/span&gt;
              &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MediaQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EdgeInsets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;white&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;child&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;WebView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="na"&gt;initialUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="na"&gt;javascriptMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JavascriptMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unrestricted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Flutter;Webview&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;navigationDelegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                  &lt;span class="c1"&gt;//Listen for callback URL&lt;/span&gt;
                  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://standard.paystack.co/close&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;Navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;//close webview&lt;/span&gt;

                    &lt;span class="c1"&gt;// _txnRef is my glabally created variable to handle my reference&lt;/span&gt;
                    &lt;span class="nx"&gt;_txnRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_ref&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="nf"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                  &lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;
                      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github.com/gikwegbu?trxref=$_ref&amp;amp;reference=$_ref&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;Navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                    &lt;span class="nx"&gt;_txnRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_ref&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="nf"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                  &lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.google.com/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;Navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                  &lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NavigationDecision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;));&lt;/span&gt;
          &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The _submit(), is a function that sends the reference to 
my backend to complete the purchase transaction on the 
selected item.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  💾  Listening For Navigation url &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;From the above code snippet, you'd have noticed the navigationDelegate section, this is where you'll handle listening for the callback_url and also, in the case the user is making payments with a Card, and the 3Ds Authentication will have to redirect you to the '&lt;a href="https://standard.paystack.co/close" rel="noopener noreferrer"&gt;https://standard.paystack.co/close&lt;/a&gt;'. Other than that, we will listen for our custom callback_url, and then pop our screen.&lt;/p&gt;
&lt;h3&gt;
  
  
  💙 Important Note For Release apk &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The application will actually work while in debug mode, but will through an error or not work at all in released build, as you'd only be seeing a gray overlay.&lt;/p&gt;
&lt;h4&gt;
  
  
  Steps Taken to Resolve this.
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Add
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;uses&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt; &lt;span class="nl"&gt;android:&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"android.permission.INTERNET"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;in your &lt;code&gt;android/app/src/main/AndroidManifest.xml&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Platform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAndroid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WebView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AndroidWebView&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside of your &lt;code&gt;initState(){}&lt;/code&gt; function of your file. This means you'd have to be using a "Stateful" widget.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;This is the most important. Run
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;flutter&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would help you build your app as in 'release mode', but you can still be seeing your debugs in your console. Now watch closely if you see an error like &lt;code&gt;I/flutter (18782): Another exception was thrown: Instance of 'DiagnosticsProperty&amp;lt;void&amp;gt;'&lt;/code&gt;, if so, then please look for where &lt;code&gt;Expanded&lt;/code&gt; widget was used in your implementation and remove it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finally
&lt;/h3&gt;

&lt;p&gt;We have successfully integrated our paystack payment system with multiply option into our flutter application. To experience the multiple option, please change from your test secret key to your live secret key, only then will you see the &lt;/p&gt;

</description>
      <category>flutter</category>
      <category>paystack</category>
      <category>android</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Unhandled exception: ProcessException: No such file or directory, Command PhaseScriptExecution failed with a nonzero exit code</title>
      <dc:creator>George Ikwegbu Chinedu</dc:creator>
      <pubDate>Fri, 10 Jun 2022 11:28:00 +0000</pubDate>
      <link>https://dev.to/gikwegbu/unhandled-exception-processexception-no-such-file-or-directory-command-phasescriptexecution-failed-with-a-nonzero-exit-code-3349</link>
      <guid>https://dev.to/gikwegbu/unhandled-exception-processexception-no-such-file-or-directory-command-phasescriptexecution-failed-with-a-nonzero-exit-code-3349</guid>
      <description>&lt;p&gt;&lt;a href="https://www.pexels.com/photo/view-of-snowy-mountains-12365573/" rel="noopener noreferrer"&gt;Photo by eberhard grossgasteiger&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As at the time of writing this article, I had already told my parents to prepare a farmland for me, as tech is not my calling again 😂😂😂😂.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This is going to be a different pattern of writing my 
articles.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;So if you come across this article as being a solution to a similar problem as the title, well, leave a thumbs up.&lt;/p&gt;

&lt;p&gt;So I cloned a flutter project to work on, started off with installing my flutter SDK and the xcode version that the project was created with.&lt;/p&gt;

&lt;p&gt;First ran for the android, which worked without issues, but when it got to the iOS, everything went south in split second.&lt;/p&gt;

&lt;p&gt;I reached out to the Engineer handling the project initially, we went through installations and codes again, found nothing, I was advised to downgrade my xcode and try again... Long story short, I currently have xcode 13.4, 13.3, 13.3.1, 13.2.1 in my mac 😂.&lt;/p&gt;

&lt;h3&gt;
  
  
  An image of my error message:
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fav2mzsbhfwy0zwzwkctx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fav2mzsbhfwy0zwzwkctx.png" alt="An image of my error message:" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Well, none of the solutions found on stackoverflow and github issues worked for me. Which included the following:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open the terminal On the root directory of your project, run:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;flutter&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt;
   &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;flutter&lt;/span&gt; &lt;span class="nx"&gt;pub&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt;

   &lt;span class="c1"&gt;// cd into your ios directory&lt;/span&gt;
   &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;cd&lt;/span&gt; &lt;span class="nx"&gt;ios&lt;/span&gt;
   &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;pod&lt;/span&gt; &lt;span class="nx"&gt;deintegrate&lt;/span&gt;
   &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;pod&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt;

   &lt;span class="c1"&gt;// cd out of your ios directory&lt;/span&gt;
   &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;cd&lt;/span&gt; &lt;span class="p"&gt;..&lt;/span&gt;

   &lt;span class="c1"&gt;// try running the project on your ios simulator&lt;/span&gt;
   &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;flutter&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;
   &lt;span class="c1"&gt;// Be sure your ios simulator is active&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having done all of the above, and even did flutter doctor, just to confirm if I had issues with my installations and setup, nothing worked for me.&lt;/p&gt;

&lt;p&gt;The most confusing thing was that, a dummy flutter project I created worked just fine, same as a flutter project I had created with my windows some months back and cloned into my mac, all worked out just fine for both Android and iOS.&lt;/p&gt;

&lt;p&gt;I found a regex code&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grep -riL "&amp;lt;path to your flutter sdk&amp;gt;" "&amp;lt;path to your project&amp;gt;" 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;this searches through your project for instances of the flutter sdk in that project. I saw logs for the dummy project and that of the flutter project created with my window, but for this current project, it was showing &lt;strong&gt;'No such directory found'&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Well, my last resort was to open an issue on github, which I reluctantly did, as I saw thousands of opened issues, didn't even expect anyone to look at mine...&lt;/p&gt;

&lt;p&gt;Well let's say someone did, and instantly noticed the problem.&lt;/p&gt;

&lt;h1&gt;
  
  
  Solution
&lt;/h1&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The URL of the cloned project, had a space after the name, 
and git used same pattern to create the directory on my 
machine and this caused the whole problem.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Below is a link to the issue I opened and closed 💁🏽.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/flutter/flutter/issues/105762" rel="noopener noreferrer"&gt;Link to Github Issue&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hope it helps.    &lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>beginners</category>
      <category>ios</category>
    </item>
  </channel>
</rss>
