Repository: zulip/zulip-flutter (language: dart)

Issue:
Show a welcome dialog on upgrading from legacy app
When this app launches for the first time in an install that previously held the legacy app (following the launch, cf #715), we should show a brief dialog box to acknowledge the obvious that the app will look quite different, and to offer more information.

Here's text for the dialog box (written by @alya):

> ### Welcome to the new Zulip app!
>
> You’ll find a familiar experience in a faster, sleeker package. Check out the [announcement blog post](https://)!
>
> [Let’s go]

The link will be to a URL under `https://blog.zulip.com/…` which we've picked in advance, and which will redirect to the blog post once it goes up. (The blog post won't yet be up when we're preparing the release build, so its actual URL won't yet be final, hence the indirection.)

A key step in this issue is to determine whether the app was upgraded from the legacy app. We'll have that information as a side effect of #1070, so I expect to implement this issue next after that one.



Discussion hints:
#1285 blocks doing links in prose, if it's translated. (In fact I'm not sure offhand how to make a link in _un_-translated text, except in the context of Zulip message content, and we don't have #488 yet.)

We can give the dialog an extra action button though. (For the implementation, maybe `showSuggestedActionDialog` does what we'd want?)
Hmm true.

Given that, I think the most expedient thing is if the link is a whole paragraph of its own. Then it can be its own Text widget, wrapped in a gesture detector with an onPressed that opens the URL. (And a separate string for translation.)

To fit that constraint, how about:
> You’ll find a familiar experience in a faster, sleeker package.
>
> [Check out the announcement blog post!](https://blog.zulip.com/)



Pull request: Welcome dialog on upgrade from legacy app
Fixes #1580.

Stacked atop #1588.

I believe this works correctly. Some tests would be nice to write, but are less critical than for #1070 / #1588.

### Screenshots
| dark | light |
| --- | --- |
| ![Screenshot_20250614_235102](https://github.com/user-attachments/assets/270c8ae0-34cd-40cb-941d-92f223c8919a) | ![Screenshot_20250614_235205](https://github.com/user-attachments/assets/efba7666-3874-4c78-851a-5b685a36052e) |


Test command: flutter test test/model/database_test.dart test/widgets/dialog_test.dart --reporter expanded 2>&1

Fix patch:
diff --git a/lib/model/database.dart b/lib/model/database.dart
index 2811ae27d2..ca84fc949c 100644
--- a/lib/model/database.dart
+++ b/lib/model/database.dart
@@ -31,6 +31,9 @@ class GlobalSettings extends Table {
   Column<String> get markReadOnScroll => textEnum<MarkReadOnScrollSetting>()
     .nullable()();
 
+  Column<String> get legacyUpgradeState => textEnum<LegacyUpgradeState>()
+    .nullable()();
+
   // If adding a new column to this table, consider whether [BoolGlobalSettings]
   // can do the job instead (by adding a value to the [BoolGlobalSetting] enum).
   // That way is more convenient, when it works, because
@@ -126,7 +129,7 @@ class AppDatabase extends _$AppDatabase {
   //    information on using the build_runner.
   //  * Write a migration in `_migrationSteps` below.
   //  * Write tests.
-  static const int latestSchemaVersion = 8; // See note.
+  static const int latestSchemaVersion = 9; // See note.
 
   @override
   int get schemaVersion => latestSchemaVersion;
@@ -189,6 +192,15 @@ class AppDatabase extends _$AppDatabase {
       await m.addColumn(schema.globalSettings,
         schema.globalSettings.markReadOnScroll);
     },
+    from8To9: (m, schema) async {
+      await m.addColumn(schema.globalSettings,
+        schema.globalSettings.legacyUpgradeState);
+      // Earlier versions of this app weren't built to be installed over
+      // the legacy app.  So if upgrading from an earlier version of this app,
+      // assume there wasn't also the legacy app before that.
+      await m.database.update(schema.globalSettings).write(
+        RawValuesInsertable({'legacy_upgrade_state': Constant('noLegacy')}));
+    }
   );
 
   Future<void> _createLatestSchema(Migrator m) async {
@@ -196,6 +208,7 @@ class AppDatabase extends _$AppDatabase {
     await m.createAll();
     // Corresponds to `from4to5` above.
     await into(globalSettings).insert(GlobalSettingsCompanion());
+    // Corresponds to (but differs from) part of `from8To9` above.
     await migrateLegacyAppData(this);
   }
 
diff --git a/lib/model/database.g.dart b/lib/model/database.g.dart
index d78f7ede84..6fdbec74f8 100644
--- a/lib/model/database.g.dart
+++ b/lib/model/database.g.dart
@@ -57,11 +57,24 @@ class $GlobalSettingsTable extends GlobalSettings
         $GlobalSettingsTable.$convertermarkReadOnScrolln,
       );
   @override
+  late final GeneratedColumnWithTypeConverter<LegacyUpgradeState?, String>
+  legacyUpgradeState =
+      GeneratedColumn<String>(
+        'legacy_upgrade_state',
+        aliasedName,
+        true,
+        type: DriftSqlType.string,
+        requiredDuringInsert: false,
+      ).withConverter<LegacyUpgradeState?>(
+        $GlobalSettingsTable.$converterlegacyUpgradeStaten,
+      );
+  @override
   List<GeneratedColumn> get $columns => [
     themeSetting,
     browserPreference,
     visitFirstUnread,
     markReadOnScroll,
+    legacyUpgradeState,
   ];
   @override
   String get aliasedName => _alias ?? actualTableName;
@@ -101,6 +114,13 @@ class $GlobalSettingsTable extends GlobalSettings
               data['${effectivePrefix}mark_read_on_scroll'],
             ),
           ),
+      legacyUpgradeState: $GlobalSettingsTable.$converterlegacyUpgradeStaten
+          .fromSql(
+            attachedDatabase.typeMapping.read(
+              DriftSqlType.string,
+              data['${effectivePrefix}legacy_upgrade_state'],
+            ),
+          ),
     );
   }
 
@@ -141,6 +161,14 @@ class $GlobalSettingsTable extends GlobalSettings
   $convertermarkReadOnScrolln = JsonTypeConverter2.asNullable(
     $convertermarkReadOnScroll,
   );
+  static JsonTypeConverter2<LegacyUpgradeState, String, String>
+  $converterlegacyUpgradeState = const EnumNameConverter<LegacyUpgradeState>(
+    LegacyUpgradeState.values,
+  );
+  static JsonTypeConverter2<LegacyUpgradeState?, String?, String?>
+  $converterlegacyUpgradeStaten = JsonTypeConverter2.asNullable(
+    $converterlegacyUpgradeState,
+  );
 }
 
 class GlobalSettingsData extends DataClass
@@ -149,11 +177,13 @@ class GlobalSettingsData extends DataClass
   final BrowserPreference? browserPreference;
   final VisitFirstUnreadSetting? visitFirstUnread;
   final MarkReadOnScrollSetting? markReadOnScroll;
+  final LegacyUpgradeState? legacyUpgradeState;
   const GlobalSettingsData({
     this.themeSetting,
     this.browserPreference,
     this.visitFirstUnread,
     this.markReadOnScroll,
+    this.legacyUpgradeState,
   });
   @override
   Map<String, Expression> toColumns(bool nullToAbsent) {
@@ -184,6 +214,13 @@ class GlobalSettingsData extends DataClass
         ),
       );
     }
+    if (!nullToAbsent || legacyUpgradeState != null) {
+      map['legacy_upgrade_state'] = Variable<String>(
+        $GlobalSettingsTable.$converterlegacyUpgradeStaten.toSql(
+          legacyUpgradeState,
+        ),
+      );
+    }
     return map;
   }
 
@@ -201,6 +238,9 @@ class GlobalSettingsData extends DataClass
       markReadOnScroll: markReadOnScroll == null && nullToAbsent
           ? const Value.absent()
           : Value(markReadOnScroll),
+      legacyUpgradeState: legacyUpgradeState == null && nullToAbsent
+          ? const Value.absent()
+          : Value(legacyUpgradeState),
     );
   }
 
@@ -219,6 +259,8 @@ class GlobalSettingsData extends DataClass
           .fromJson(serializer.fromJson<String?>(json['visitFirstUnread'])),
       markReadOnScroll: $GlobalSettingsTable.$convertermarkReadOnScrolln
           .fromJson(serializer.fromJson<String?>(json['markReadOnScroll'])),
+      legacyUpgradeState: $GlobalSettingsTable.$converterlegacyUpgradeStaten
+          .fromJson(serializer.fromJson<String?>(json['legacyUpgradeState'])),
     );
   }
   @override
@@ -243,6 +285,11 @@ class GlobalSettingsData extends DataClass
           markReadOnScroll,
         ),
       ),
+      'legacyUpgradeState': serializer.toJson<String?>(
+        $GlobalSettingsTable.$converterlegacyUpgradeStaten.toJson(
+          legacyUpgradeState,
+        ),
+      ),
     };
   }
 
@@ -251,6 +298,7 @@ class GlobalSettingsData extends DataClass
     Value<BrowserPreference?> browserPreference = const Value.absent(),
     Value<VisitFirstUnreadSetting?> visitFirstUnread = const Value.absent(),
     Value<MarkReadOnScrollSetting?> markReadOnScroll = const Value.absent(),
+    Value<LegacyUpgradeState?> legacyUpgradeState = const Value.absent(),
   }) => GlobalSettingsData(
     themeSetting: themeSetting.present ? themeSetting.value : this.themeSetting,
     browserPreference: browserPreference.present
@@ -262,6 +310,9 @@ class GlobalSettingsData extends DataClass
     markReadOnScroll: markReadOnScroll.present
         ? markReadOnScroll.value
         : this.markReadOnScroll,
+    legacyUpgradeState: legacyUpgradeState.present
+        ? legacyUpgradeState.value
+        : this.legacyUpgradeState,
   );
   GlobalSettingsData copyWithCompanion(GlobalSettingsCompanion data) {
     return GlobalSettingsData(
@@ -277,6 +328,9 @@ class GlobalSettingsData extends DataClass
       markReadOnScroll: data.markReadOnScroll.present
           ? data.markReadOnScroll.value
           : this.markReadOnScroll,
+      legacyUpgradeState: data.legacyUpgradeState.present
+          ? data.legacyUpgradeState.value
+          : this.legacyUpgradeState,
     );
   }
 
@@ -286,7 +340,8 @@ class GlobalSettingsData extends DataClass
           ..write('themeSetting: $themeSetting, ')
           ..write('browserPreference: $browserPreference, ')
           ..write('visitFirstUnread: $visitFirstUnread, ')
-          ..write('markReadOnScroll: $markReadOnScroll')
+          ..write('markReadOnScroll: $markReadOnScroll, ')
+          ..write('legacyUpgradeState: $legacyUpgradeState')
           ..write(')'))
         .toString();
   }
@@ -297,6 +352,7 @@ class GlobalSettingsData extends DataClass
     browserPreference,
     visitFirstUnread,
     markReadOnScroll,
+    legacyUpgradeState,
   );
   @override
   bool operator ==(Object other) =>
@@ -305,7 +361,8 @@ class GlobalSettingsData extends DataClass
           other.themeSetting == this.themeSetting &&
           other.browserPreference == this.browserPreference &&
           other.visitFirstUnread == this.visitFirstUnread &&
-          other.markReadOnScroll == this.markReadOnScroll);
+          other.markReadOnScroll == this.markReadOnScroll &&
+          other.legacyUpgradeState == this.legacyUpgradeState);
 }
 
 class GlobalSettingsCompanion extends UpdateCompanion<GlobalSettingsData> {
@@ -313,12 +370,14 @@ class GlobalSettingsCompanion extends UpdateCompanion<GlobalSettingsData> {
   final Value<BrowserPreference?> browserPreference;
   final Value<VisitFirstUnreadSetting?> visitFirstUnread;
   final Value<MarkReadOnScrollSetting?> markReadOnScroll;
+  final Value<LegacyUpgradeState?> legacyUpgradeState;
   final Value<int> rowid;
   const GlobalSettingsCompanion({
     this.themeSetting = const Value.absent(),
     this.browserPreference = const Value.absent(),
     this.visitFirstUnread = const Value.absent(),
     this.markReadOnScroll = const Value.absent(),
+    this.legacyUpgradeState = const Value.absent(),
     this.rowid = const Value.absent(),
   });
   GlobalSettingsCompanion.insert({
@@ -326,6 +385,7 @@ class GlobalSettingsCompanion extends UpdateCompanion<GlobalSettingsData> {
     this.browserPreference = const Value.absent(),
     this.visitFirstUnread = const Value.absent(),
     this.markReadOnScroll = const Value.absent(),
+    this.legacyUpgradeState = const Value.absent(),
     this.rowid = const Value.absent(),
   });
   static Insertable<GlobalSettingsData> custom({
@@ -333,6 +393,7 @@ class GlobalSettingsCompanion extends UpdateCompanion<GlobalSettingsData> {
     Expression<String>? browserPreference,
     Expression<String>? visitFirstUnread,
     Expression<String>? markReadOnScroll,
+    Expression<String>? legacyUpgradeState,
     Expression<int>? rowid,
   }) {
     return RawValuesInsertable({
@@ -340,6 +401,8 @@ class GlobalSettingsCompanion extends UpdateCompanion<GlobalSettingsData> {
       if (browserPreference != null) 'browser_preference': browserPreference,
       if (visitFirstUnread != null) 'visit_first_unread': visitFirstUnread,
       if (markReadOnScroll != null) 'mark_read_on_scroll': markReadOnScroll,
+      if (legacyUpgradeState != null)
+        'legacy_upgrade_state': legacyUpgradeState,
       if (rowid != null) 'rowid': rowid,
     });
   }
@@ -349,6 +412,7 @@ class GlobalSettingsCompanion extends UpdateCompanion<GlobalSettingsData> {
     Value<BrowserPreference?>? browserPreference,
     Value<VisitFirstUnreadSetting?>? visitFirstUnread,
     Value<MarkReadOnScrollSetting?>? markReadOnScroll,
+    Value<LegacyUpgradeState?>? legacyUpgradeState,
     Value<int>? rowid,
   }) {
     return GlobalSettingsCompanion(
@@ -356,6 +420,7 @@ class GlobalSettingsCompanion extends UpdateCompanion<GlobalSettingsData> {
       browserPreference: browserPreference ?? this.browserPreference,
       visitFirstUnread: visitFirstUnread ?? this.visitFirstUnread,
       markReadOnScroll: markReadOnScroll ?? this.markReadOnScroll,
+      legacyUpgradeState: legacyUpgradeState ?? this.legacyUpgradeState,
       rowid: rowid ?? this.rowid,
     );
   }
@@ -389,6 +454,13 @@ class GlobalSettingsCompanion extends UpdateCompanion<GlobalSettingsData> {
         ),
       );
     }
+    if (legacyUpgradeState.present) {
+      map['legacy_upgrade_state'] = Variable<String>(
+        $GlobalSettingsTable.$converterlegacyUpgradeStaten.toSql(
+          legacyUpgradeState.value,
+        ),
+      );
+    }
     if (rowid.present) {
       map['rowid'] = Variable<int>(rowid.value);
     }
@@ -402,6 +474,7 @@ class GlobalSettingsCompanion extends UpdateCompanion<GlobalSettingsData> {
           ..write('browserPreference: $browserPreference, ')
           ..write('visitFirstUnread: $visitFirstUnread, ')
           ..write('markReadOnScroll: $markReadOnScroll, ')
+          ..write('legacyUpgradeState: $legacyUpgradeState, ')
           ..write('rowid: $rowid')
           ..write(')'))
         .toString();
@@ -1248,6 +1321,7 @@ typedef $$GlobalSettingsTableCreateCompanionBuilder =
       Value<BrowserPreference?> browserPreference,
       Value<VisitFirstUnreadSetting?> visitFirstUnread,
       Value<MarkReadOnScrollSetting?> markReadOnScroll,
+      Value<LegacyUpgradeState?> legacyUpgradeState,
       Value<int> rowid,
     });
 typedef $$GlobalSettingsTableUpdateCompanionBuilder =
@@ -1256,6 +1330,7 @@ typedef $$GlobalSettingsTableUpdateCompanionBuilder =
       Value<BrowserPreference?> browserPreference,
       Value<VisitFirstUnreadSetting?> visitFirstUnread,
       Value<MarkReadOnScrollSetting?> markReadOnScroll,
+      Value<LegacyUpgradeState?> legacyUpgradeState,
       Value<int> rowid,
     });
 
@@ -1299,6 +1374,16 @@ class $$GlobalSettingsTableFilterComposer
     column: $table.markReadOnScroll,
     builder: (column) => ColumnWithTypeConverterFilters(column),
   );
+
+  ColumnWithTypeConverterFilters<
+    LegacyUpgradeState?,
+    LegacyUpgradeState,
+    String
+  >
+  get legacyUpgradeState => $composableBuilder(
+    column: $table.legacyUpgradeState,
+    builder: (column) => ColumnWithTypeConverterFilters(column),
+  );
 }
 
 class $$GlobalSettingsTableOrderingComposer
@@ -1329,6 +1414,11 @@ class $$GlobalSettingsTableOrderingComposer
     column: $table.markReadOnScroll,
     builder: (column) => ColumnOrderings(column),
   );
+
+  ColumnOrderings<String> get legacyUpgradeState => $composableBuilder(
+    column: $table.legacyUpgradeState,
+    builder: (column) => ColumnOrderings(column),
+  );
 }
 
 class $$GlobalSettingsTableAnnotationComposer
@@ -1363,6 +1453,12 @@ class $$GlobalSettingsTableAnnotationComposer
     column: $table.markReadOnScroll,
     builder: (column) => column,
   );
+
+  GeneratedColumnWithTypeConverter<LegacyUpgradeState?, String>
+  get legacyUpgradeState => $composableBuilder(
+    column: $table.legacyUpgradeState,
+    builder: (column) => column,
+  );
 }
 
 class $$GlobalSettingsTableTableManager
@@ -1409,12 +1505,15 @@ class $$GlobalSettingsTableTableManager
                     const Value.absent(),
                 Value<MarkReadOnScrollSetting?> markReadOnScroll =
                     const Value.absent(),
+                Value<LegacyUpgradeState?> legacyUpgradeState =
+                    const Value.absent(),
                 Value<int> rowid = const Value.absent(),
               }) => GlobalSettingsCompanion(
                 themeSetting: themeSetting,
                 browserPreference: browserPreference,
                 visitFirstUnread: visitFirstUnread,
                 markReadOnScroll: markReadOnScroll,
+                legacyUpgradeState: legacyUpgradeState,
                 rowid: rowid,
               ),
           createCompanionCallback:
@@ -1426,12 +1525,15 @@ class $$GlobalSettingsTableTableManager
                     const Value.absent(),
                 Value<MarkReadOnScrollSetting?> markReadOnScroll =
                     const Value.absent(),
+                Value<LegacyUpgradeState?> legacyUpgradeState =
+                    const Value.absent(),
                 Value<int> rowid = const Value.absent(),
               }) => GlobalSettingsCompanion.insert(
                 themeSetting: themeSetting,
                 browserPreference: browserPreference,
                 visitFirstUnread: visitFirstUnread,
                 markReadOnScroll: markReadOnScroll,
+                legacyUpgradeState: legacyUpgradeState,
                 rowid: rowid,
               ),
           withReferenceMapper: (p0) => p0
diff --git a/lib/model/legacy_app_data.dart b/lib/model/legacy_app_data.dart
index f3b94395ff..215248c776 100644
--- a/lib/model/legacy_app_data.dart
+++ b/lib/model/legacy_app_data.dart
@@ -26,10 +26,12 @@ Future<void> migrateLegacyAppData(AppDatabase db) async {
   final legacyData = await readLegacyAppData();
   if (legacyData == null) {
     assert(debugLog("... no legacy app data found."));
+    await _setLegacyUpgradeState(db, LegacyUpgradeState.noLegacy);
     return;
   }
 
   assert(debugLog("Found settings: ${legacyData.settings?.toJson()}"));
+  await _setLegacyUpgradeState(db, LegacyUpgradeState.found);
   final settings = legacyData.settings;
   if (settings != null) {
     await db.update(db.globalSettings).write(GlobalSettingsCompanion(
@@ -106,6 +108,12 @@ Future<void> migrateLegacyAppData(AppDatabase db) async {
   }
 
   assert(debugLog("Done migrating legacy app data."));
+  await _setLegacyUpgradeState(db, LegacyUpgradeState.migrated);
+}
+
+Future<void> _setLegacyUpgradeState(AppDatabase db, LegacyUpgradeState value) async {
+  await db.update(db.globalSettings).write(GlobalSettingsCompanion(
+    legacyUpgradeState: drift.Value(value)));
 }
 
 Future<LegacyAppData?> readLegacyAppData() async {
diff --git a/lib/model/schema_versions.g.dart b/lib/model/schema_versions.g.dart
index 5712a94fbb..782b9409e2 100644
--- a/lib/model/schema_versions.g.dart
+++ b/lib/model/schema_versions.g.dart
@@ -517,6 +517,84 @@ i1.GeneratedColumn<String> _column_14(String aliasedName) =>
       true,
       type: i1.DriftSqlType.string,
     );
+
+final class Schema9 extends i0.VersionedSchema {
+  Schema9({required super.database}) : super(version: 9);
+  @override
+  late final List<i1.DatabaseSchemaEntity> entities = [
+    globalSettings,
+    boolGlobalSettings,
+    accounts,
+  ];
+  late final Shape6 globalSettings = Shape6(
+    source: i0.VersionedTable(
+      entityName: 'global_settings',
+      withoutRowId: false,
+      isStrict: false,
+      tableConstraints: [],
+      columns: [_column_9, _column_10, _column_13, _column_14, _column_15],
+      attachedDatabase: database,
+    ),
+    alias: null,
+  );
+  late final Shape3 boolGlobalSettings = Shape3(
+    source: i0.VersionedTable(
+      entityName: 'bool_global_settings',
+      withoutRowId: false,
+      isStrict: false,
+      tableConstraints: ['PRIMARY KEY(name)'],
+      columns: [_column_11, _column_12],
+      attachedDatabase: database,
+    ),
+    alias: null,
+  );
+  late final Shape0 accounts = Shape0(
+    source: i0.VersionedTable(
+      entityName: 'accounts',
+      withoutRowId: false,
+      isStrict: false,
+      tableConstraints: [
+        'UNIQUE(realm_url, user_id)',
+        'UNIQUE(realm_url, email)',
+      ],
+      columns: [
+        _column_0,
+        _column_1,
+        _column_2,
+        _column_3,
+        _column_4,
+        _column_5,
+        _column_6,
+        _column_7,
+        _column_8,
+      ],
+      attachedDatabase: database,
+    ),
+    alias: null,
+  );
+}
+
+class Shape6 extends i0.VersionedTable {
+  Shape6({required super.source, required super.alias}) : super.aliased();
+  i1.GeneratedColumn<String> get themeSetting =>
+      columnsByName['theme_setting']! as i1.GeneratedColumn<String>;
+  i1.GeneratedColumn<String> get browserPreference =>
+      columnsByName['browser_preference']! as i1.GeneratedColumn<String>;
+  i1.GeneratedColumn<String> get visitFirstUnread =>
+      columnsByName['visit_first_unread']! as i1.GeneratedColumn<String>;
+  i1.GeneratedColumn<String> get markReadOnScroll =>
+      columnsByName['mark_read_on_scroll']! as i1.GeneratedColumn<String>;
+  i1.GeneratedColumn<String> get legacyUpgradeState =>
+      columnsByName['legacy_upgrade_state']! as i1.GeneratedColumn<String>;
+}
+
+i1.GeneratedColumn<String> _column_15(String aliasedName) =>
+    i1.GeneratedColumn<String>(
+      'legacy_upgrade_state',
+      aliasedName,
+      true,
+      type: i1.DriftSqlType.string,
+    );
 i0.MigrationStepWithVersion migrationSteps({
   required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
   required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -525,6 +603,7 @@ i0.MigrationStepWithVersion migrationSteps({
   required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
   required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
   required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
+  required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
 }) {
   return (currentVersion, database) async {
     switch (currentVersion) {
@@ -563,6 +642,11 @@ i0.MigrationStepWithVersion migrationSteps({
         final migrator = i1.Migrator(database, schema);
         await from7To8(migrator, schema);
         return 8;
+      case 8:
+        final schema = Schema9(database: database);
+        final migrator = i1.Migrator(database, schema);
+        await from8To9(migrator, schema);
+        return 9;
       default:
         throw ArgumentError.value('Unknown migration from $currentVersion');
     }
@@ -577,6 +661,7 @@ i1.OnUpgrade stepByStep({
   required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
   required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
   required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
+  required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
 }) => i0.VersionedSchema.stepByStepHelper(
   step: migrationSteps(
     from1To2: from1To2,
@@ -586,5 +671,6 @@ i1.OnUpgrade stepByStep({
     from5To6: from5To6,
     from6To7: from6To7,
     from7To8: from7To8,
+    from8To9: from8To9,
   ),
 );
diff --git a/lib/model/settings.dart b/lib/model/settings.dart
index 298980a395..81777d988d 100644
--- a/lib/model/settings.dart
+++ b/lib/model/settings.dart
@@ -87,6 +87,24 @@ enum MarkReadOnScrollSetting {
   static MarkReadOnScrollSetting _default = conversations;
 }
 
+/// The outcome, or in-progress status, of migrating data from the legacy app.
+enum LegacyUpgradeState {
+  /// It's not yet known whether there was data from the legacy app.
+  unknown,
+
+  /// No legacy data was found.
+  noLegacy,
+
+  /// Legacy data was found, but not yet migrated into this app's database.
+  found,
+
+  /// Legacy data was found and migrated.
+  migrated,
+  ;
+
+  static LegacyUpgradeState _default = unknown;
+}
+
 /// A general category of account-independent setting the user might set.
 ///
 /// Different kinds of settings call for different treatment in the UI,
@@ -101,6 +119,9 @@ enum GlobalSettingType {
   /// we give it a placeholder value which isn't a real setting.
   placeholder,
 
+  /// Describes a pseudo-setting not directly exposed in the UI.
+  internal,
+
   /// Describes a setting which enables an in-progress feature of the app.
   ///
   /// Sometimes when building a complex feature it's useful to merge PRs that
@@ -152,6 +173,10 @@ enum BoolGlobalSetting {
   /// (Having one stable value in this enum is also handy for tests.)
   placeholderIgnore(GlobalSettingType.placeholder, false),
 
+  /// A pseudo-setting recording whether the user has been shown the
+  /// welcome dialog for upgrading from the legacy app.
+  upgradeWelcomeDialogShown(GlobalSettingType.internal, false),
+
   /// An experimental flag to toggle rendering KaTeX content in messages.
   renderKatex(GlobalSettingType.experimentalFeatureFlag, false),
 
@@ -324,6 +349,11 @@ class GlobalSettingsStore extends ChangeNotifier {
     };
   }
 
+  /// The outcome, or in-progress status, of migrating data from the legacy app.
+  LegacyUpgradeState get legacyUpgradeState {
+    return _data.legacyUpgradeState ?? LegacyUpgradeState._default;
+  }
+
   /// The user's choice of the given bool-valued setting, or our default for it.
   ///
   /// See also [setBool].
diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart
index b1aa763ac8..d3ed5c463d 100644
--- a/lib/widgets/app.dart
+++ b/lib/widgets/app.dart
@@ -160,6 +160,7 @@ class _ZulipAppState extends State<ZulipApp> with WidgetsBindingObserver {
   void initState() {
     super.initState();
     WidgetsBinding.instance.addObserver(this);
+    UpgradeWelcomeDialog.maybeShow();
   }
 
   @override
diff --git a/lib/widgets/dialog.dart b/lib/widgets/dialog.dart
index 4d269cddba..e635071cc9 100644
--- a/lib/widgets/dialog.dart
+++ b/lib/widgets/dialog.dart
@@ -1,7 +1,11 @@
 import 'package:flutter/material.dart';
 
 import '../generated/l10n/zulip_localizations.dart';
+import '../model/settings.dart';
 import 'actions.dart';
+import 'app.dart';
+import 'content.dart';
+import 'store.dart';
 
 Widget _dialogActionText(String text) {
   return Text(
@@ -112,3 +116,69 @@ DialogStatus<bool> showSuggestedActionDialog({
       ]));
   return DialogStatus(future);
 }
+
+/// A brief dialog box welcoming the user to this new Zulip app,
+/// shown upon upgrading from the legacy app.
+class UpgradeWelcomeDialog extends StatelessWidget {
+  const UpgradeWelcomeDialog._();
+
+  static void maybeShow() async {
+    final navigator = await ZulipApp.navigator;
+    final context = navigator.context;
+    assert(context.mounted);
+    if (!context.mounted) return; // TODO(linter): this is impossible as there's no actual async gap, but the use_build_context_synchronously lint doesn't see that
+
+    final globalSettings = GlobalStoreWidget.settingsOf(context);
+    switch (globalSettings.legacyUpgradeState) {
+      case LegacyUpgradeState.noLegacy:
+        // This install didn't replace the legacy app.
+        return;
+
+      case LegacyUpgradeState.unknown:
+        // Not clear if this replaced the legacy app;
+        // skip the dialog that would assume it had.
+        // TODO(log)
+        return;
+
+      case LegacyUpgradeState.found:
+      case LegacyUpgradeState.migrated:
+        // This install replaced the legacy app.
+        // Show the dialog, if we haven't already.
+        if (globalSettings.getBool(BoolGlobalSetting.upgradeWelcomeDialogShown)) {
+          return;
+        }
+    }
+
+    final future = showDialog<void>(
+      context: context,
+      builder: (context) => UpgradeWelcomeDialog._());
+
+    await future; // Wait for the dialog to be dismissed.
+
+    await globalSettings.setBool(BoolGlobalSetting.upgradeWelcomeDialogShown, true);
+  }
+
+  static const String _announcementUrl =
+    'https://blog.zulip.com/flutter-mobile-app-launch';
+
+  @override
+  Widget build(BuildContext context) {
+    final zulipLocalizations = ZulipLocalizations.of(context);
+    return AlertDialog(
+      title: Text(zulipLocalizations.upgradeWelcomeDialogTitle),
+      content: SingleChildScrollView(
+        child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
+          Text(zulipLocalizations.upgradeWelcomeDialogMessage),
+          GestureDetector(
+            onTap: () => PlatformActions.launchUrl(context,
+              Uri.parse(_announcementUrl)),
+            child: Text(
+              style: TextStyle(color: ContentTheme.of(context).colorLink),
+              zulipLocalizations.upgradeWelcomeDialogLinkText)),
+        ])),
+      actions: [
+        TextButton(onPressed: () => Navigator.pop(context),
+          child: Text(zulipLocalizations.upgradeWelcomeDialogDismiss)),
+      ]);
+  }
+}


Test patch:
diff --git a/test/model/database_test.dart b/test/model/database_test.dart
index e6e2b729be..e89bd569db 100644
--- a/test/model/database_test.dart
+++ b/test/model/database_test.dart
@@ -326,6 +326,8 @@ void main() {
       check(globalSettings.browserPreference).isNull();
       await after.close();
     });
+
+    // TODO(#1593) test upgrade to v9: legacyUpgradeState set to noLegacy
   });
 }
 
diff --git a/test/model/schemas/drift_schema_v9.json b/test/model/schemas/drift_schema_v9.json
new file mode 100644
index 0000000000..e425bc89c8
--- /dev/null
+++ b/test/model/schemas/drift_schema_v9.json
@@ -0,0 +1,1 @@
+{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"global_settings","was_declared_in_moor":false,"columns":[{"name":"theme_setting","getter_name":"themeSetting","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<ThemeSetting>(ThemeSetting.values)","dart_type_name":"ThemeSetting"}},{"name":"browser_preference","getter_name":"browserPreference","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<BrowserPreference>(BrowserPreference.values)","dart_type_name":"BrowserPreference"}},{"name":"visit_first_unread","getter_name":"visitFirstUnread","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<VisitFirstUnreadSetting>(VisitFirstUnreadSetting.values)","dart_type_name":"VisitFirstUnreadSetting"}},{"name":"mark_read_on_scroll","getter_name":"markReadOnScroll","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<MarkReadOnScrollSetting>(MarkReadOnScrollSetting.values)","dart_type_name":"MarkReadOnScrollSetting"}},{"name":"legacy_upgrade_state","getter_name":"legacyUpgradeState","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter<LegacyUpgradeState>(LegacyUpgradeState.values)","dart_type_name":"LegacyUpgradeState"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"bool_global_settings","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"value","getter_name":"value","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"value\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"value\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["name"]}},{"id":2,"references":[],"type":"table","data":{"name":"accounts","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"realm_url","getter_name":"realmUrl","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const UriConverter()","dart_type_name":"Uri"}},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"api_key","getter_name":"apiKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_version","getter_name":"zulipVersion","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_merge_base","getter_name":"zulipMergeBase","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_feature_level","getter_name":"zulipFeatureLevel","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"acked_push_token","getter_name":"ackedPushToken","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"unique_keys":[["realm_url","user_id"],["realm_url","email"]]}}]}
\ No newline at end of file
diff --git a/test/model/schemas/schema.dart b/test/model/schemas/schema.dart
index 746206e453..413b4408c4 100644
--- a/test/model/schemas/schema.dart
+++ b/test/model/schemas/schema.dart
@@ -11,6 +11,7 @@ import 'schema_v5.dart' as v5;
 import 'schema_v6.dart' as v6;
 import 'schema_v7.dart' as v7;
 import 'schema_v8.dart' as v8;
+import 'schema_v9.dart' as v9;
 
 class GeneratedHelper implements SchemaInstantiationHelper {
   @override
@@ -32,10 +33,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
         return v7.DatabaseAtV7(db);
       case 8:
         return v8.DatabaseAtV8(db);
+      case 9:
+        return v9.DatabaseAtV9(db);
       default:
         throw MissingSchemaException(version, versions);
     }
   }
 
-  static const versions = const [1, 2, 3, 4, 5, 6, 7, 8];
+  static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9];
 }
diff --git a/test/model/schemas/schema_v9.dart b/test/model/schemas/schema_v9.dart
new file mode 100644
index 0000000000..d036e3a26f
--- /dev/null
+++ b/test/model/schemas/schema_v9.dart
@@ -0,0 +1,1014 @@
+// dart format width=80
+// GENERATED CODE, DO NOT EDIT BY HAND.
+// ignore_for_file: type=lint
+import 'package:drift/drift.dart';
+
+class GlobalSettings extends Table
+    with TableInfo<GlobalSettings, GlobalSettingsData> {
+  @override
+  final GeneratedDatabase attachedDatabase;
+  final String? _alias;
+  GlobalSettings(this.attachedDatabase, [this._alias]);
+  late final GeneratedColumn<String> themeSetting = GeneratedColumn<String>(
+    'theme_setting',
+    aliasedName,
+    true,
+    type: DriftSqlType.string,
+    requiredDuringInsert: false,
+  );
+  late final GeneratedColumn<String> browserPreference =
+      GeneratedColumn<String>(
+        'browser_preference',
+        aliasedName,
+        true,
+        type: DriftSqlType.string,
+        requiredDuringInsert: false,
+      );
+  late final GeneratedColumn<String> visitFirstUnread = GeneratedColumn<String>(
+    'visit_first_unread',
+    aliasedName,
+    true,
+    type: DriftSqlType.string,
+    requiredDuringInsert: false,
+  );
+  late final GeneratedColumn<String> markReadOnScroll = GeneratedColumn<String>(
+    'mark_read_on_scroll',
+    aliasedName,
+    true,
+    type: DriftSqlType.string,
+    requiredDuringInsert: false,
+  );
+  late final GeneratedColumn<String> legacyUpgradeState =
+      GeneratedColumn<String>(
+        'legacy_upgrade_state',
+        aliasedName,
+        true,
+        type: DriftSqlType.string,
+        requiredDuringInsert: false,
+      );
+  @override
+  List<GeneratedColumn> get $columns => [
+    themeSetting,
+    browserPreference,
+    visitFirstUnread,
+    markReadOnScroll,
+    legacyUpgradeState,
+  ];
+  @override
+  String get aliasedName => _alias ?? actualTableName;
+  @override
+  String get actualTableName => $name;
+  static const String $name = 'global_settings';
+  @override
+  Set<GeneratedColumn> get $primaryKey => const {};
+  @override
+  GlobalSettingsData map(Map<String, dynamic> data, {String? tablePrefix}) {
+    final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+    return GlobalSettingsData(
+      themeSetting: attachedDatabase.typeMapping.read(
+        DriftSqlType.string,
+        data['${effectivePrefix}theme_setting'],
+      ),
+      browserPreference: attachedDatabase.typeMapping.read(
+        DriftSqlType.string,
+        data['${effectivePrefix}browser_preference'],
+      ),
+      visitFirstUnread: attachedDatabase.typeMapping.read(
+        DriftSqlType.string,
+        data['${effectivePrefix}visit_first_unread'],
+      ),
+      markReadOnScroll: attachedDatabase.typeMapping.read(
+        DriftSqlType.string,
+        data['${effectivePrefix}mark_read_on_scroll'],
+      ),
+      legacyUpgradeState: attachedDatabase.typeMapping.read(
+        DriftSqlType.string,
+        data['${effectivePrefix}legacy_upgrade_state'],
+      ),
+    );
+  }
+
+  @override
+  GlobalSettings createAlias(String alias) {
+    return GlobalSettings(attachedDatabase, alias);
+  }
+}
+
+class GlobalSettingsData extends DataClass
+    implements Insertable<GlobalSettingsData> {
+  final String? themeSetting;
+  final String? browserPreference;
+  final String? visitFirstUnread;
+  final String? markReadOnScroll;
+  final String? legacyUpgradeState;
+  const GlobalSettingsData({
+    this.themeSetting,
+    this.browserPreference,
+    this.visitFirstUnread,
+    this.markReadOnScroll,
+    this.legacyUpgradeState,
+  });
+  @override
+  Map<String, Expression> toColumns(bool nullToAbsent) {
+    final map = <String, Expression>{};
+    if (!nullToAbsent || themeSetting != null) {
+      map['theme_setting'] = Variable<String>(themeSetting);
+    }
+    if (!nullToAbsent || browserPreference != null) {
+      map['browser_preference'] = Variable<String>(browserPreference);
+    }
+    if (!nullToAbsent || visitFirstUnread != null) {
+      map['visit_first_unread'] = Variable<String>(visitFirstUnread);
+    }
+    if (!nullToAbsent || markReadOnScroll != null) {
+      map['mark_read_on_scroll'] = Variable<String>(markReadOnScroll);
+    }
+    if (!nullToAbsent || legacyUpgradeState != null) {
+      map['legacy_upgrade_state'] = Variable<String>(legacyUpgradeState);
+    }
+    return map;
+  }
+
+  GlobalSettingsCompanion toCompanion(bool nullToAbsent) {
+    return GlobalSettingsCompanion(
+      themeSetting: themeSetting == null && nullToAbsent
+          ? const Value.absent()
+          : Value(themeSetting),
+      browserPreference: browserPreference == null && nullToAbsent
+          ? const Value.absent()
+          : Value(browserPreference),
+      visitFirstUnread: visitFirstUnread == null && nullToAbsent
+          ? const Value.absent()
+          : Value(visitFirstUnread),
+      markReadOnScroll: markReadOnScroll == null && nullToAbsent
+          ? const Value.absent()
+          : Value(markReadOnScroll),
+      legacyUpgradeState: legacyUpgradeState == null && nullToAbsent
+          ? const Value.absent()
+          : Value(legacyUpgradeState),
+    );
+  }
+
+  factory GlobalSettingsData.fromJson(
+    Map<String, dynamic> json, {
+    ValueSerializer? serializer,
+  }) {
+    serializer ??= driftRuntimeOptions.defaultSerializer;
+    return GlobalSettingsData(
+      themeSetting: serializer.fromJson<String?>(json['themeSetting']),
+      browserPreference: serializer.fromJson<String?>(
+        json['browserPreference'],
+      ),
+      visitFirstUnread: serializer.fromJson<String?>(json['visitFirstUnread']),
+      markReadOnScroll: serializer.fromJson<String?>(json['markReadOnScroll']),
+      legacyUpgradeState: serializer.fromJson<String?>(
+        json['legacyUpgradeState'],
+      ),
+    );
+  }
+  @override
+  Map<String, dynamic> toJson({ValueSerializer? serializer}) {
+    serializer ??= driftRuntimeOptions.defaultSerializer;
+    return <String, dynamic>{
+      'themeSetting': serializer.toJson<String?>(themeSetting),
+      'browserPreference': serializer.toJson<String?>(browserPreference),
+      'visitFirstUnread': serializer.toJson<String?>(visitFirstUnread),
+      'markReadOnScroll': serializer.toJson<String?>(markReadOnScroll),
+      'legacyUpgradeState': serializer.toJson<String?>(legacyUpgradeState),
+    };
+  }
+
+  GlobalSettingsData copyWith({
+    Value<String?> themeSetting = const Value.absent(),
+    Value<String?> browserPreference = const Value.absent(),
+    Value<String?> visitFirstUnread = const Value.absent(),
+    Value<String?> markReadOnScroll = const Value.absent(),
+    Value<String?> legacyUpgradeState = const Value.absent(),
+  }) => GlobalSettingsData(
+    themeSetting: themeSetting.present ? themeSetting.value : this.themeSetting,
+    browserPreference: browserPreference.present
+        ? browserPreference.value
+        : this.browserPreference,
+    visitFirstUnread: visitFirstUnread.present
+        ? visitFirstUnread.value
+        : this.visitFirstUnread,
+    markReadOnScroll: markReadOnScroll.present
+        ? markReadOnScroll.value
+        : this.markReadOnScroll,
+    legacyUpgradeState: legacyUpgradeState.present
+        ? legacyUpgradeState.value
+        : this.legacyUpgradeState,
+  );
+  GlobalSettingsData copyWithCompanion(GlobalSettingsCompanion data) {
+    return GlobalSettingsData(
+      themeSetting: data.themeSetting.present
+          ? data.themeSetting.value
+          : this.themeSetting,
+      browserPreference: data.browserPreference.present
+          ? data.browserPreference.value
+          : this.browserPreference,
+      visitFirstUnread: data.visitFirstUnread.present
+          ? data.visitFirstUnread.value
+          : this.visitFirstUnread,
+      markReadOnScroll: data.markReadOnScroll.present
+          ? data.markReadOnScroll.value
+          : this.markReadOnScroll,
+      legacyUpgradeState: data.legacyUpgradeState.present
+          ? data.legacyUpgradeState.value
+          : this.legacyUpgradeState,
+    );
+  }
+
+  @override
+  String toString() {
+    return (StringBuffer('GlobalSettingsData(')
+          ..write('themeSetting: $themeSetting, ')
+          ..write('browserPreference: $browserPreference, ')
+          ..write('visitFirstUnread: $visitFirstUnread, ')
+          ..write('markReadOnScroll: $markReadOnScroll, ')
+          ..write('legacyUpgradeState: $legacyUpgradeState')
+          ..write(')'))
+        .toString();
+  }
+
+  @override
+  int get hashCode => Object.hash(
+    themeSetting,
+    browserPreference,
+    visitFirstUnread,
+    markReadOnScroll,
+    legacyUpgradeState,
+  );
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      (other is GlobalSettingsData &&
+          other.themeSetting == this.themeSetting &&
+          other.browserPreference == this.browserPreference &&
+          other.visitFirstUnread == this.visitFirstUnread &&
+          other.markReadOnScroll == this.markReadOnScroll &&
+          other.legacyUpgradeState == this.legacyUpgradeState);
+}
+
+class GlobalSettingsCompanion extends UpdateCompanion<GlobalSettingsData> {
+  final Value<String?> themeSetting;
+  final Value<String?> browserPreference;
+  final Value<String?> visitFirstUnread;
+  final Value<String?> markReadOnScroll;
+  final Value<String?> legacyUpgradeState;
+  final Value<int> rowid;
+  const GlobalSettingsCompanion({
+    this.themeSetting = const Value.absent(),
+    this.browserPreference = const Value.absent(),
+    this.visitFirstUnread = const Value.absent(),
+    this.markReadOnScroll = const Value.absent(),
+    this.legacyUpgradeState = const Value.absent(),
+    this.rowid = const Value.absent(),
+  });
+  GlobalSettingsCompanion.insert({
+    this.themeSetting = const Value.absent(),
+    this.browserPreference = const Value.absent(),
+    this.visitFirstUnread = const Value.absent(),
+    this.markReadOnScroll = const Value.absent(),
+    this.legacyUpgradeState = const Value.absent(),
+    this.rowid = const Value.absent(),
+  });
+  static Insertable<GlobalSettingsData> custom({
+    Expression<String>? themeSetting,
+    Expression<String>? browserPreference,
+    Expression<String>? visitFirstUnread,
+    Expression<String>? markReadOnScroll,
+    Expression<String>? legacyUpgradeState,
+    Expression<int>? rowid,
+  }) {
+    return RawValuesInsertable({
+      if (themeSetting != null) 'theme_setting': themeSetting,
+      if (browserPreference != null) 'browser_preference': browserPreference,
+      if (visitFirstUnread != null) 'visit_first_unread': visitFirstUnread,
+      if (markReadOnScroll != null) 'mark_read_on_scroll': markReadOnScroll,
+      if (legacyUpgradeState != null)
+        'legacy_upgrade_state': legacyUpgradeState,
+      if (rowid != null) 'rowid': rowid,
+    });
+  }
+
+  GlobalSettingsCompanion copyWith({
+    Value<String?>? themeSetting,
+    Value<String?>? browserPreference,
+    Value<String?>? visitFirstUnread,
+    Value<String?>? markReadOnScroll,
+    Value<String?>? legacyUpgradeState,
+    Value<int>? rowid,
+  }) {
+    return GlobalSettingsCompanion(
+      themeSetting: themeSetting ?? this.themeSetting,
+      browserPreference: browserPreference ?? this.browserPreference,
+      visitFirstUnread: visitFirstUnread ?? this.visitFirstUnread,
+      markReadOnScroll: markReadOnScroll ?? this.markReadOnScroll,
+      legacyUpgradeState: legacyUpgradeState ?? this.legacyUpgradeState,
+      rowid: rowid ?? this.rowid,
+    );
+  }
+
+  @override
+  Map<String, Expression> toColumns(bool nullToAbsent) {
+    final map = <String, Expression>{};
+    if (themeSetting.present) {
+      map['theme_setting'] = Variable<String>(themeSetting.value);
+    }
+    if (browserPreference.present) {
+      map['browser_preference'] = Variable<String>(browserPreference.value);
+    }
+    if (visitFirstUnread.present) {
+      map['visit_first_unread'] = Variable<String>(visitFirstUnread.value);
+    }
+    if (markReadOnScroll.present) {
+      map['mark_read_on_scroll'] = Variable<String>(markReadOnScroll.value);
+    }
+    if (legacyUpgradeState.present) {
+      map['legacy_upgrade_state'] = Variable<String>(legacyUpgradeState.value);
+    }
+    if (rowid.present) {
+      map['rowid'] = Variable<int>(rowid.value);
+    }
+    return map;
+  }
+
+  @override
+  String toString() {
+    return (StringBuffer('GlobalSettingsCompanion(')
+          ..write('themeSetting: $themeSetting, ')
+          ..write('browserPreference: $browserPreference, ')
+          ..write('visitFirstUnread: $visitFirstUnread, ')
+          ..write('markReadOnScroll: $markReadOnScroll, ')
+          ..write('legacyUpgradeState: $legacyUpgradeState, ')
+          ..write('rowid: $rowid')
+          ..write(')'))
+        .toString();
+  }
+}
+
+class BoolGlobalSettings extends Table
+    with TableInfo<BoolGlobalSettings, BoolGlobalSettingsData> {
+  @override
+  final GeneratedDatabase attachedDatabase;
+  final String? _alias;
+  BoolGlobalSettings(this.attachedDatabase, [this._alias]);
+  late final GeneratedColumn<String> name = GeneratedColumn<String>(
+    'name',
+    aliasedName,
+    false,
+    type: DriftSqlType.string,
+    requiredDuringInsert: true,
+  );
+  late final GeneratedColumn<bool> value = GeneratedColumn<bool>(
+    'value',
+    aliasedName,
+    false,
+    type: DriftSqlType.bool,
+    requiredDuringInsert: true,
+    defaultConstraints: GeneratedColumn.constraintIsAlways(
+      'CHECK ("value" IN (0, 1))',
+    ),
+  );
+  @override
+  List<GeneratedColumn> get $columns => [name, value];
+  @override
+  String get aliasedName => _alias ?? actualTableName;
+  @override
+  String get actualTableName => $name;
+  static const String $name = 'bool_global_settings';
+  @override
+  Set<GeneratedColumn> get $primaryKey => {name};
+  @override
+  BoolGlobalSettingsData map(Map<String, dynamic> data, {String? tablePrefix}) {
+    final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+    return BoolGlobalSettingsData(
+      name: attachedDatabase.typeMapping.read(
+        DriftSqlType.string,
+        data['${effectivePrefix}name'],
+      )!,
+      value: attachedDatabase.typeMapping.read(
+        DriftSqlType.bool,
+        data['${effectivePrefix}value'],
+      )!,
+    );
+  }
+
+  @override
+  BoolGlobalSettings createAlias(String alias) {
+    return BoolGlobalSettings(attachedDatabase, alias);
+  }
+}
+
+class BoolGlobalSettingsData extends DataClass
+    implements Insertable<BoolGlobalSettingsData> {
+  final String name;
+  final bool value;
+  const BoolGlobalSettingsData({required this.name, required this.value});
+  @override
+  Map<String, Expression> toColumns(bool nullToAbsent) {
+    final map = <String, Expression>{};
+    map['name'] = Variable<String>(name);
+    map['value'] = Variable<bool>(value);
+    return map;
+  }
+
+  BoolGlobalSettingsCompanion toCompanion(bool nullToAbsent) {
+    return BoolGlobalSettingsCompanion(name: Value(name), value: Value(value));
+  }
+
+  factory BoolGlobalSettingsData.fromJson(
+    Map<String, dynamic> json, {
+    ValueSerializer? serializer,
+  }) {
+    serializer ??= driftRuntimeOptions.defaultSerializer;
+    return BoolGlobalSettingsData(
+      name: serializer.fromJson<String>(json['name']),
+      value: serializer.fromJson<bool>(json['value']),
+    );
+  }
+  @override
+  Map<String, dynamic> toJson({ValueSerializer? serializer}) {
+    serializer ??= driftRuntimeOptions.defaultSerializer;
+    return <String, dynamic>{
+      'name': serializer.toJson<String>(name),
+      'value': serializer.toJson<bool>(value),
+    };
+  }
+
+  BoolGlobalSettingsData copyWith({String? name, bool? value}) =>
+      BoolGlobalSettingsData(
+        name: name ?? this.name,
+        value: value ?? this.value,
+      );
+  BoolGlobalSettingsData copyWithCompanion(BoolGlobalSettingsCompanion data) {
+    return BoolGlobalSettingsData(
+      name: data.name.present ? data.name.value : this.name,
+      value: data.value.present ? data.value.value : this.value,
+    );
+  }
+
+  @override
+  String toString() {
+    return (StringBuffer('BoolGlobalSettingsData(')
+          ..write('name: $name, ')
+          ..write('value: $value')
+          ..write(')'))
+        .toString();
+  }
+
+  @override
+  int get hashCode => Object.hash(name, value);
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      (other is BoolGlobalSettingsData &&
+          other.name == this.name &&
+          other.value == this.value);
+}
+
+class BoolGlobalSettingsCompanion
+    extends UpdateCompanion<BoolGlobalSettingsData> {
+  final Value<String> name;
+  final Value<bool> value;
+  final Value<int> rowid;
+  const BoolGlobalSettingsCompanion({
+    this.name = const Value.absent(),
+    this.value = const Value.absent(),
+    this.rowid = const Value.absent(),
+  });
+  BoolGlobalSettingsCompanion.insert({
+    required String name,
+    required bool value,
+    this.rowid = const Value.absent(),
+  }) : name = Value(name),
+       value = Value(value);
+  static Insertable<BoolGlobalSettingsData> custom({
+    Expression<String>? name,
+    Expression<bool>? value,
+    Expression<int>? rowid,
+  }) {
+    return RawValuesInsertable({
+      if (name != null) 'name': name,
+      if (value != null) 'value': value,
+      if (rowid != null) 'rowid': rowid,
+    });
+  }
+
+  BoolGlobalSettingsCompanion copyWith({
+    Value<String>? name,
+    Value<bool>? value,
+    Value<int>? rowid,
+  }) {
+    return BoolGlobalSettingsCompanion(
+      name: name ?? this.name,
+      value: value ?? this.value,
+      rowid: rowid ?? this.rowid,
+    );
+  }
+
+  @override
+  Map<String, Expression> toColumns(bool nullToAbsent) {
+    final map = <String, Expression>{};
+    if (name.present) {
+      map['name'] = Variable<String>(name.value);
+    }
+    if (value.present) {
+      map['value'] = Variable<bool>(value.value);
+    }
+    if (rowid.present) {
+      map['rowid'] = Variable<int>(rowid.value);
+    }
+    return map;
+  }
+
+  @override
+  String toString() {
+    return (StringBuffer('BoolGlobalSettingsCompanion(')
+          ..write('name: $name, ')
+          ..write('value: $value, ')
+          ..write('rowid: $rowid')
+          ..write(')'))
+        .toString();
+  }
+}
+
+class Accounts extends Table with TableInfo<Accounts, AccountsData> {
+  @override
+  final GeneratedDatabase attachedDatabase;
+  final String? _alias;
+  Accounts(this.attachedDatabase, [this._alias]);
+  late final GeneratedColumn<int> id = GeneratedColumn<int>(
+    'id',
+    aliasedName,
+    false,
+    hasAutoIncrement: true,
+    type: DriftSqlType.int,
+    requiredDuringInsert: false,
+    defaultConstraints: GeneratedColumn.constraintIsAlways(
+      'PRIMARY KEY AUTOINCREMENT',
+    ),
+  );
+  late final GeneratedColumn<String> realmUrl = GeneratedColumn<String>(
+    'realm_url',
+    aliasedName,
+    false,
+    type: DriftSqlType.string,
+    requiredDuringInsert: true,
+  );
+  late final GeneratedColumn<int> userId = GeneratedColumn<int>(
+    'user_id',
+    aliasedName,
+    false,
+    type: DriftSqlType.int,
+    requiredDuringInsert: true,
+  );
+  late final GeneratedColumn<String> email = GeneratedColumn<String>(
+    'email',
+    aliasedName,
+    false,
+    type: DriftSqlType.string,
+    requiredDuringInsert: true,
+  );
+  late final GeneratedColumn<String> apiKey = GeneratedColumn<String>(
+    'api_key',
+    aliasedName,
+    false,
+    type: DriftSqlType.string,
+    requiredDuringInsert: true,
+  );
+  late final GeneratedColumn<String> zulipVersion = GeneratedColumn<String>(
+    'zulip_version',
+    aliasedName,
+    false,
+    type: DriftSqlType.string,
+    requiredDuringInsert: true,
+  );
+  late final GeneratedColumn<String> zulipMergeBase = GeneratedColumn<String>(
+    'zulip_merge_base',
+    aliasedName,
+    true,
+    type: DriftSqlType.string,
+    requiredDuringInsert: false,
+  );
+  late final GeneratedColumn<int> zulipFeatureLevel = GeneratedColumn<int>(
+    'zulip_feature_level',
+    aliasedName,
+    false,
+    type: DriftSqlType.int,
+    requiredDuringInsert: true,
+  );
+  late final GeneratedColumn<String> ackedPushToken = GeneratedColumn<String>(
+    'acked_push_token',
+    aliasedName,
+    true,
+    type: DriftSqlType.string,
+    requiredDuringInsert: false,
+  );
+  @override
+  List<GeneratedColumn> get $columns => [
+    id,
+    realmUrl,
+    userId,
+    email,
+    apiKey,
+    zulipVersion,
+    zulipMergeBase,
+    zulipFeatureLevel,
+    ackedPushToken,
+  ];
+  @override
+  String get aliasedName => _alias ?? actualTableName;
+  @override
+  String get actualTableName => $name;
+  static const String $name = 'accounts';
+  @override
+  Set<GeneratedColumn> get $primaryKey => {id};
+  @override
+  List<Set<GeneratedColumn>> get uniqueKeys => [
+    {realmUrl, userId},
+    {realmUrl, email},
+  ];
+  @override
+  AccountsData map(Map<String, dynamic> data, {String? tablePrefix}) {
+    final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+    return AccountsData(
+      id: attachedDatabase.typeMapping.read(
+        DriftSqlType.int,
+        data['${effectivePrefix}id'],
+      )!,
+      realmUrl: attachedDatabase.typeMapping.read(
+        DriftSqlType.string,
+        data['${effectivePrefix}realm_url'],
+      )!,
+      userId: attachedDatabase.typeMapping.read(
+        DriftSqlType.int,
+        data['${effectivePrefix}user_id'],
+      )!,
+      email: attachedDatabase.typeMapping.read(
+        DriftSqlType.string,
+        data['${effectivePrefix}email'],
+      )!,
+      apiKey: attachedDatabase.typeMapping.read(
+        DriftSqlType.string,
+        data['${effectivePrefix}api_key'],
+      )!,
+      zulipVersion: attachedDatabase.typeMapping.read(
+        DriftSqlType.string,
+        data['${effectivePrefix}zulip_version'],
+      )!,
+      zulipMergeBase: attachedDatabase.typeMapping.read(
+        DriftSqlType.string,
+        data['${effectivePrefix}zulip_merge_base'],
+      ),
+      zulipFeatureLevel: attachedDatabase.typeMapping.read(
+        DriftSqlType.int,
+        data['${effectivePrefix}zulip_feature_level'],
+      )!,
+      ackedPushToken: attachedDatabase.typeMapping.read(
+        DriftSqlType.string,
+        data['${effectivePrefix}acked_push_token'],
+      ),
+    );
+  }
+
+  @override
+  Accounts createAlias(String alias) {
+    return Accounts(attachedDatabase, alias);
+  }
+}
+
+class AccountsData extends DataClass implements Insertable<AccountsData> {
+  final int id;
+  final String realmUrl;
+  final int userId;
+  final String email;
+  final String apiKey;
+  final String zulipVersion;
+  final String? zulipMergeBase;
+  final int zulipFeatureLevel;
+  final String? ackedPushToken;
+  const AccountsData({
+    required this.id,
+    required this.realmUrl,
+    required this.userId,
+    required this.email,
+    required this.apiKey,
+    required this.zulipVersion,
+    this.zulipMergeBase,
+    required this.zulipFeatureLevel,
+    this.ackedPushToken,
+  });
+  @override
+  Map<String, Expression> toColumns(bool nullToAbsent) {
+    final map = <String, Expression>{};
+    map['id'] = Variable<int>(id);
+    map['realm_url'] = Variable<String>(realmUrl);
+    map['user_id'] = Variable<int>(userId);
+    map['email'] = Variable<String>(email);
+    map['api_key'] = Variable<String>(apiKey);
+    map['zulip_version'] = Variable<String>(zulipVersion);
+    if (!nullToAbsent || zulipMergeBase != null) {
+      map['zulip_merge_base'] = Variable<String>(zulipMergeBase);
+    }
+    map['zulip_feature_level'] = Variable<int>(zulipFeatureLevel);
+    if (!nullToAbsent || ackedPushToken != null) {
+      map['acked_push_token'] = Variable<String>(ackedPushToken);
+    }
+    return map;
+  }
+
+  AccountsCompanion toCompanion(bool nullToAbsent) {
+    return AccountsCompanion(
+      id: Value(id),
+      realmUrl: Value(realmUrl),
+      userId: Value(userId),
+      email: Value(email),
+      apiKey: Value(apiKey),
+      zulipVersion: Value(zulipVersion),
+      zulipMergeBase: zulipMergeBase == null && nullToAbsent
+          ? const Value.absent()
+          : Value(zulipMergeBase),
+      zulipFeatureLevel: Value(zulipFeatureLevel),
+      ackedPushToken: ackedPushToken == null && nullToAbsent
+          ? const Value.absent()
+          : Value(ackedPushToken),
+    );
+  }
+
+  factory AccountsData.fromJson(
+    Map<String, dynamic> json, {
+    ValueSerializer? serializer,
+  }) {
+    serializer ??= driftRuntimeOptions.defaultSerializer;
+    return AccountsData(
+      id: serializer.fromJson<int>(json['id']),
+      realmUrl: serializer.fromJson<String>(json['realmUrl']),
+      userId: serializer.fromJson<int>(json['userId']),
+      email: serializer.fromJson<String>(json['email']),
+      apiKey: serializer.fromJson<String>(json['apiKey']),
+      zulipVersion: serializer.fromJson<String>(json['zulipVersion']),
+      zulipMergeBase: serializer.fromJson<String?>(json['zulipMergeBase']),
+      zulipFeatureLevel: serializer.fromJson<int>(json['zulipFeatureLevel']),
+      ackedPushToken: serializer.fromJson<String?>(json['ackedPushToken']),
+    );
+  }
+  @override
+  Map<String, dynamic> toJson({ValueSerializer? serializer}) {
+    serializer ??= driftRuntimeOptions.defaultSerializer;
+    return <String, dynamic>{
+      'id': serializer.toJson<int>(id),
+      'realmUrl': serializer.toJson<String>(realmUrl),
+      'userId': serializer.toJson<int>(userId),
+      'email': serializer.toJson<String>(email),
+      'apiKey': serializer.toJson<String>(apiKey),
+      'zulipVersion': serializer.toJson<String>(zulipVersion),
+      'zulipMergeBase': serializer.toJson<String?>(zulipMergeBase),
+      'zulipFeatureLevel': serializer.toJson<int>(zulipFeatureLevel),
+      'ackedPushToken': serializer.toJson<String?>(ackedPushToken),
+    };
+  }
+
+  AccountsData copyWith({
+    int? id,
+    String? realmUrl,
+    int? userId,
+    String? email,
+    String? apiKey,
+    String? zulipVersion,
+    Value<String?> zulipMergeBase = const Value.absent(),
+    int? zulipFeatureLevel,
+    Value<String?> ackedPushToken = const Value.absent(),
+  }) => AccountsData(
+    id: id ?? this.id,
+    realmUrl: realmUrl ?? this.realmUrl,
+    userId: userId ?? this.userId,
+    email: email ?? this.email,
+    apiKey: apiKey ?? this.apiKey,
+    zulipVersion: zulipVersion ?? this.zulipVersion,
+    zulipMergeBase: zulipMergeBase.present
+        ? zulipMergeBase.value
+        : this.zulipMergeBase,
+    zulipFeatureLevel: zulipFeatureLevel ?? this.zulipFeatureLevel,
+    ackedPushToken: ackedPushToken.present
+        ? ackedPushToken.value
+        : this.ackedPushToken,
+  );
+  AccountsData copyWithCompanion(AccountsCompanion data) {
+    return AccountsData(
+      id: data.id.present ? data.id.value : this.id,
+      realmUrl: data.realmUrl.present ? data.realmUrl.value : this.realmUrl,
+      userId: data.userId.present ? data.userId.value : this.userId,
+      email: data.email.present ? data.email.value : this.email,
+      apiKey: data.apiKey.present ? data.apiKey.value : this.apiKey,
+      zulipVersion: data.zulipVersion.present
+          ? data.zulipVersion.value
+          : this.zulipVersion,
+      zulipMergeBase: data.zulipMergeBase.present
+          ? data.zulipMergeBase.value
+          : this.zulipMergeBase,
+      zulipFeatureLevel: data.zulipFeatureLevel.present
+          ? data.zulipFeatureLevel.value
+          : this.zulipFeatureLevel,
+      ackedPushToken: data.ackedPushToken.present
+          ? data.ackedPushToken.value
+          : this.ackedPushToken,
+    );
+  }
+
+  @override
+  String toString() {
+    return (StringBuffer('AccountsData(')
+          ..write('id: $id, ')
+          ..write('realmUrl: $realmUrl, ')
+          ..write('userId: $userId, ')
+          ..write('email: $email, ')
+          ..write('apiKey: $apiKey, ')
+          ..write('zulipVersion: $zulipVersion, ')
+          ..write('zulipMergeBase: $zulipMergeBase, ')
+          ..write('zulipFeatureLevel: $zulipFeatureLevel, ')
+          ..write('ackedPushToken: $ackedPushToken')
+          ..write(')'))
+        .toString();
+  }
+
+  @override
+  int get hashCode => Object.hash(
+    id,
+    realmUrl,
+    userId,
+    email,
+    apiKey,
+    zulipVersion,
+    zulipMergeBase,
+    zulipFeatureLevel,
+    ackedPushToken,
+  );
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      (other is AccountsData &&
+          other.id == this.id &&
+          other.realmUrl == this.realmUrl &&
+          other.userId == this.userId &&
+          other.email == this.email &&
+          other.apiKey == this.apiKey &&
+          other.zulipVersion == this.zulipVersion &&
+          other.zulipMergeBase == this.zulipMergeBase &&
+          other.zulipFeatureLevel == this.zulipFeatureLevel &&
+          other.ackedPushToken == this.ackedPushToken);
+}
+
+class AccountsCompanion extends UpdateCompanion<AccountsData> {
+  final Value<int> id;
+  final Value<String> realmUrl;
+  final Value<int> userId;
+  final Value<String> email;
+  final Value<String> apiKey;
+  final Value<String> zulipVersion;
+  final Value<String?> zulipMergeBase;
+  final Value<int> zulipFeatureLevel;
+  final Value<String?> ackedPushToken;
+  const AccountsCompanion({
+    this.id = const Value.absent(),
+    this.realmUrl = const Value.absent(),
+    this.userId = const Value.absent(),
+    this.email = const Value.absent(),
+    this.apiKey = const Value.absent(),
+    this.zulipVersion = const Value.absent(),
+    this.zulipMergeBase = const Value.absent(),
+    this.zulipFeatureLevel = const Value.absent(),
+    this.ackedPushToken = const Value.absent(),
+  });
+  AccountsCompanion.insert({
+    this.id = const Value.absent(),
+    required String realmUrl,
+    required int userId,
+    required String email,
+    required String apiKey,
+    required String zulipVersion,
+    this.zulipMergeBase = const Value.absent(),
+    required int zulipFeatureLevel,
+    this.ackedPushToken = const Value.absent(),
+  }) : realmUrl = Value(realmUrl),
+       userId = Value(userId),
+       email = Value(email),
+       apiKey = Value(apiKey),
+       zulipVersion = Value(zulipVersion),
+       zulipFeatureLevel = Value(zulipFeatureLevel);
+  static Insertable<AccountsData> custom({
+    Expression<int>? id,
+    Expression<String>? realmUrl,
+    Expression<int>? userId,
+    Expression<String>? email,
+    Expression<String>? apiKey,
+    Expression<String>? zulipVersion,
+    Expression<String>? zulipMergeBase,
+    Expression<int>? zulipFeatureLevel,
+    Expression<String>? ackedPushToken,
+  }) {
+    return RawValuesInsertable({
+      if (id != null) 'id': id,
+      if (realmUrl != null) 'realm_url': realmUrl,
+      if (userId != null) 'user_id': userId,
+      if (email != null) 'email': email,
+      if (apiKey != null) 'api_key': apiKey,
+      if (zulipVersion != null) 'zulip_version': zulipVersion,
+      if (zulipMergeBase != null) 'zulip_merge_base': zulipMergeBase,
+      if (zulipFeatureLevel != null) 'zulip_feature_level': zulipFeatureLevel,
+      if (ackedPushToken != null) 'acked_push_token': ackedPushToken,
+    });
+  }
+
+  AccountsCompanion copyWith({
+    Value<int>? id,
+    Value<String>? realmUrl,
+    Value<int>? userId,
+    Value<String>? email,
+    Value<String>? apiKey,
+    Value<String>? zulipVersion,
+    Value<String?>? zulipMergeBase,
+    Value<int>? zulipFeatureLevel,
+    Value<String?>? ackedPushToken,
+  }) {
+    return AccountsCompanion(
+      id: id ?? this.id,
+      realmUrl: realmUrl ?? this.realmUrl,
+      userId: userId ?? this.userId,
+      email: email ?? this.email,
+      apiKey: apiKey ?? this.apiKey,
+      zulipVersion: zulipVersion ?? this.zulipVersion,
+      zulipMergeBase: zulipMergeBase ?? this.zulipMergeBase,
+      zulipFeatureLevel: zulipFeatureLevel ?? this.zulipFeatureLevel,
+      ackedPushToken: ackedPushToken ?? this.ackedPushToken,
+    );
+  }
+
+  @override
+  Map<String, Expression> toColumns(bool nullToAbsent) {
+    final map = <String, Expression>{};
+    if (id.present) {
+      map['id'] = Variable<int>(id.value);
+    }
+    if (realmUrl.present) {
+      map['realm_url'] = Variable<String>(realmUrl.value);
+    }
+    if (userId.present) {
+      map['user_id'] = Variable<int>(userId.value);
+    }
+    if (email.present) {
+      map['email'] = Variable<String>(email.value);
+    }
+    if (apiKey.present) {
+      map['api_key'] = Variable<String>(apiKey.value);
+    }
+    if (zulipVersion.present) {
+      map['zulip_version'] = Variable<String>(zulipVersion.value);
+    }
+    if (zulipMergeBase.present) {
+      map['zulip_merge_base'] = Variable<String>(zulipMergeBase.value);
+    }
+    if (zulipFeatureLevel.present) {
+      map['zulip_feature_level'] = Variable<int>(zulipFeatureLevel.value);
+    }
+    if (ackedPushToken.present) {
+      map['acked_push_token'] = Variable<String>(ackedPushToken.value);
+    }
+    return map;
+  }
+
+  @override
+  String toString() {
+    return (StringBuffer('AccountsCompanion(')
+          ..write('id: $id, ')
+          ..write('realmUrl: $realmUrl, ')
+          ..write('userId: $userId, ')
+          ..write('email: $email, ')
+          ..write('apiKey: $apiKey, ')
+          ..write('zulipVersion: $zulipVersion, ')
+          ..write('zulipMergeBase: $zulipMergeBase, ')
+          ..write('zulipFeatureLevel: $zulipFeatureLevel, ')
+          ..write('ackedPushToken: $ackedPushToken')
+          ..write(')'))
+        .toString();
+  }
+}
+
+class DatabaseAtV9 extends GeneratedDatabase {
+  DatabaseAtV9(QueryExecutor e) : super(e);
+  late final GlobalSettings globalSettings = GlobalSettings(this);
+  late final BoolGlobalSettings boolGlobalSettings = BoolGlobalSettings(this);
+  late final Accounts accounts = Accounts(this);
+  @override
+  Iterable<TableInfo<Table, Object?>> get allTables =>
+      allSchemaEntities.whereType<TableInfo<Table, Object?>>();
+  @override
+  List<DatabaseSchemaEntity> get allSchemaEntities => [
+    globalSettings,
+    boolGlobalSettings,
+    accounts,
+  ];
+  @override
+  int get schemaVersion => 9;
+}
diff --git a/test/widgets/dialog_test.dart b/test/widgets/dialog_test.dart
index c86aae478e..1980f619f3 100644
--- a/test/widgets/dialog_test.dart
+++ b/test/widgets/dialog_test.dart
@@ -73,4 +73,6 @@ void main() {
       await check(dialog.result).completes((it) => it.equals(null));
     });
   });
+
+  // TODO(#1594): test UpgradeWelcomeDialog
 }