Skip to content

Commit

Permalink
Usertransfer from production(beta) to release version of the app (#359)
Browse files Browse the repository at this point in the history
* Start user transfer function

* Add migrating process for production users to enable same basis for shortcuts in hamburg

* add check migration in usertransfer window

* Fix naming

* Add migration service

* revert cocoapods

* Fix migration functions and add staging

* Fix typos and make remove change notifier on migration class

* set default backend release and change settings button and info for app version use

* Add loading screen for user transfer

* add migration test in internal settings

* wording

* Add example shortcuts to releae

* Make migration class static

* Fix race condition in usertransfer and migration
  • Loading branch information
PaulPickhardt authored Dec 1, 2023
1 parent 858fc48 commit 9032edf
Show file tree
Hide file tree
Showing 16 changed files with 887 additions and 304 deletions.
27 changes: 27 additions & 0 deletions lib/common/layout/icon_item.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:priobike/common/layout/ci.dart';
import 'package:priobike/common/layout/spacing.dart';
import 'package:priobike/common/layout/text.dart';

/// A list item with icon.
class IconItem extends Row {
IconItem({super.key, required IconData icon, required String text, required BuildContext context})
: super(
children: [
SizedBox(
width: 64,
height: 64,
child: Icon(
icon,
color: CI.radkulturRed,
size: 64,
semanticLabel: text,
),
),
const SmallHSpace(),
Expanded(
child: Content(text: text, context: context),
),
],
);
}
24 changes: 24 additions & 0 deletions lib/common/layout/loading_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';

/// Displays a loading screen.
class LoadingScreen extends StatelessWidget {
const LoadingScreen({super.key});

@override
Widget build(BuildContext context) {
var frame = MediaQuery.of(context);

return Container(
width: frame.size.width,
height: frame.size.height,
color: Colors.black.withOpacity(0.25),
child: const Center(
child: SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(),
),
),
);
}
}
50 changes: 28 additions & 22 deletions lib/home/services/shortcuts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class Shortcuts with ChangeNotifier {

final backend = getIt<Settings>().backend;
final jsonStr = jsonEncode(shortcuts!.map((e) => e.toJson()).toList());
storage.setString("priobike.home.shortcuts.${backend.name}", jsonStr);
storage.setString("priobike.home.shortcuts.${backend.regionName}", jsonStr);
}

/// Load the custom shortcuts.
Expand All @@ -121,36 +121,42 @@ class Shortcuts with ChangeNotifier {
final storage = await SharedPreferences.getInstance();

final backend = getIt<Settings>().backend;
final jsonStr = storage.getString("priobike.home.shortcuts.${backend.name}");
final jsonStr = storage.getString("priobike.home.shortcuts.${backend.regionName}");

if (jsonStr == null) {
shortcuts = backend.defaultShortcuts;
await storeShortcuts();
} else {
// Init shortcuts.
shortcuts = [];
// Loop through all json Shortcuts and add correct shortcuts to shortcuts.
for (final e in jsonDecode(jsonStr) as List) {
if (e["type"] != null) {
switch (e["type"]) {
case "ShortcutLocation":
shortcuts?.add(ShortcutLocation.fromJson(e));
break;
case "ShortcutRoute":
shortcuts?.add(ShortcutRoute.fromJson(e));
break;
default:
final hint = "Error unknown type ${e["type"]} in loadShortcuts.";
log.e(hint);
}
} else {
// Only for backwards compatibility.
if (e["waypoint"] != null) shortcuts?.add(ShortcutLocation.fromJson(e));
if (e["waypoints"] != null) shortcuts?.add(ShortcutRoute.fromJson(e));
shortcuts = getShortcutsFromJson(jsonStr);
}
notifyListeners();
}

/// Creates a list of shortcut objects from a json string.
List<Shortcut> getShortcutsFromJson(String jsonStr) {
List<Shortcut> shortcuts = [];
// Loop through all json Shortcuts and add correct shortcuts to shortcuts.
for (final e in jsonDecode(jsonStr) as List) {
if (e["type"] != null) {
switch (e["type"]) {
case "ShortcutLocation":
shortcuts.add(ShortcutLocation.fromJson(e));
break;
case "ShortcutRoute":
shortcuts.add(ShortcutRoute.fromJson(e));
break;
default:
final hint = "Error unknown type ${e["type"]} in loadShortcuts.";
log.e(hint);
}
} else {
// Only for backwards compatibility.
if (e["waypoint"] != null) shortcuts.add(ShortcutLocation.fromJson(e));
if (e["waypoints"] != null) shortcuts.add(ShortcutRoute.fromJson(e));
}
}
notifyListeners();
return shortcuts;
}

/// Delete a shortcut.
Expand Down
9 changes: 6 additions & 3 deletions lib/loader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import 'package:priobike/http.dart';
import 'package:priobike/logging/logger.dart';
import 'package:priobike/logging/toast.dart';
import 'package:priobike/main.dart';
import 'package:priobike/migration/services.dart';
import 'package:priobike/news/services/news.dart';
import 'package:priobike/ride/services/ride.dart';
import 'package:priobike/routing/services/boundary.dart';
Expand Down Expand Up @@ -70,6 +71,7 @@ class LoaderState extends State<Loader> {
// while critical services should throw their errors.

try {
await Migration.migrate();
await getIt<Profile>().loadProfile();
await getIt<Shortcuts>().loadShortcuts();
await getIt<Layers>().loadPreferences();
Expand All @@ -82,9 +84,9 @@ class LoaderState extends State<Loader> {

await getIt<News>().getArticles();

final preditionStatusSummary = getIt<PredictionStatusSummary>();
await preditionStatusSummary.fetch();
if (preditionStatusSummary.hadError) throw Exception("Error while fetching prediction status summary");
final predictionStatusSummary = getIt<PredictionStatusSummary>();
await predictionStatusSummary.fetch();
if (predictionStatusSummary.hadError) throw Exception("Error while fetching prediction status summary");

await getIt<StatusHistory>().fetch();
await getIt<Weather>().fetch();
Expand Down Expand Up @@ -114,6 +116,7 @@ class LoaderState extends State<Loader> {
hasError = false;
});
settings.resetConnectionErrorCounter();

// After a short delay, we can show the home view.
await Future.delayed(const Duration(milliseconds: 1000));
setState(() => isLoading = false);
Expand Down
9 changes: 8 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'package:priobike/home/services/profile.dart';
import 'package:priobike/home/services/shortcuts.dart';
import 'package:priobike/loader.dart';
import 'package:priobike/logging/logger.dart';
import 'package:priobike/migration/user_transfer_view.dart';
import 'package:priobike/news/services/news.dart';
import 'package:priobike/positioning/services/positioning.dart';
import 'package:priobike/privacy/services.dart';
Expand Down Expand Up @@ -155,7 +156,13 @@ class App extends StatelessWidget {
}
getIt<Shortcuts>().saveNewShortcutObject(shortcut);
}
return MaterialPageRoute(builder: (context) => const PrivacyPolicyView(child: Loader()));
return MaterialPageRoute(
builder: (context) => const PrivacyPolicyView(
child: UserTransferView(
child: Loader(),
),
),
);
},
// The navigator key is used to access the app's build context.
navigatorKey: navigatorKey,
Expand Down
213 changes: 213 additions & 0 deletions lib/migration/services.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import 'dart:convert';

import 'package:flutter/material.dart' hide Shortcuts;
import 'package:priobike/home/models/shortcut.dart';
import 'package:priobike/home/models/shortcut_location.dart';
import 'package:priobike/home/models/shortcut_route.dart';
import 'package:priobike/home/services/shortcuts.dart';
import 'package:priobike/main.dart';
import 'package:priobike/routing/models/waypoint.dart';
import 'package:priobike/settings/models/backend.dart';
import 'package:shared_preferences/shared_preferences.dart';

class Migration {
/// Load the privacy policy.
static Future<void> migrate() async {
// List of things to migrate.
await migrateShortcutsProduction();
await migrateShortcutsStaging();
await migrateSearchHistoryProduction();
await migrateSearchHistoryStaging();
}

/// Migrate all shortcuts (production/release => Hamburg).
static Future<void> migrateShortcutsProduction() async {
final storage = await SharedPreferences.getInstance();

Shortcuts shortcuts = getIt<Shortcuts>();

// Get the current shortcuts of the currently used backend.
final jsonStrProduction = storage.getString("priobike.home.shortcuts.${Backend.production.name}");
final jsonStrRelease = storage.getString("priobike.home.shortcuts.${Backend.release.name}");
// Return on no old key found.
if (jsonStrProduction == null && jsonStrRelease == null) return;
final jsonStrReleaseNew = storage.getString("priobike.home.shortcuts.${Backend.release.regionName}");

List<Shortcut> shortcutsRelease = [];
List<Shortcut> shortcutsProduction = [];
List<Shortcut> shortcutsReleaseNew = [];

if (jsonStrRelease != null) {
shortcutsRelease = shortcuts.getShortcutsFromJson(jsonStrRelease);
}

if (jsonStrProduction != null) {
shortcutsProduction = shortcuts.getShortcutsFromJson(jsonStrProduction);
}

if (jsonStrReleaseNew != null) {
shortcutsReleaseNew = shortcuts.getShortcutsFromJson(jsonStrReleaseNew);
}

// Concat all.
shortcutsReleaseNew.addAll(shortcutsRelease);
shortcutsReleaseNew.addAll(shortcutsProduction);

final jsonStr = jsonEncode(shortcutsReleaseNew.map((e) => e.toJson()).toList());

// Save shortcuts under region name (Hamburg, Dresden) so that production and release use the same shortcuts.
storage.setString("priobike.home.shortcuts.${Backend.release.regionName}", jsonStr);
// Remove the unused shortcuts.
storage.remove("priobike.home.shortcuts.${Backend.production.name}");
storage.remove("priobike.home.shortcuts.${Backend.release.name}");
}

/// Migrate all shortcuts (staging => Dresden).
static Future<void> migrateShortcutsStaging() async {
final storage = await SharedPreferences.getInstance();

Shortcuts shortcuts = getIt<Shortcuts>();

// Get the current shortcuts of the currently used backend.
final jsonStrStaging = storage.getString("priobike.home.shortcuts.${Backend.staging.name}");
// Return on no old key found.
if (jsonStrStaging == null) return;
final jsonStrStagingNew = storage.getString("priobike.home.shortcuts.${Backend.staging.regionName}");

List<Shortcut> shortcutsStagingNew = [];

List<Shortcut> shortcutsStaging = shortcuts.getShortcutsFromJson(jsonStrStaging);

if (jsonStrStagingNew != null) {
shortcutsStagingNew = shortcuts.getShortcutsFromJson(jsonStrStagingNew);
}

// Concat all.
shortcutsStagingNew.addAll(shortcutsStaging);

final jsonStr = jsonEncode(shortcutsStagingNew.map((e) => e.toJson()).toList());

// Save shortcuts under region name (Hamburg, Dresden) so that production and release use the same shortcuts.
storage.setString("priobike.home.shortcuts.${Backend.staging.regionName}", jsonStr);
// Remove the unused shortcuts.
storage.remove("priobike.home.shortcuts.${Backend.staging.name}");
}

/// Migrate the search history (production/release => Hamburg).
static Future<void> migrateSearchHistoryProduction() async {
final storage = await SharedPreferences.getInstance();

// Load production and release lists.
List<String>? searchHistoryListProduction =
storage.getStringList("priobike.routing.searchHistory.${Backend.production.name}");
// Return on no key found.
if (searchHistoryListProduction == null) return;

List<String> searchHistoryListRelease =
storage.getStringList("priobike.routing.searchHistory.${Backend.release.name}") ?? [];
// Concat both lists.
searchHistoryListRelease.addAll(searchHistoryListProduction);
// Store concatenated list.
await storage.setStringList(
"priobike.routing.searchHistory.${Backend.release.regionName}", searchHistoryListRelease);
// Remove old list.
await storage.remove("priobike.routing.searchHistory.${Backend.production.name}");
}

/// Migrate the search history (staging => Dresden).
static Future<void> migrateSearchHistoryStaging() async {
final storage = await SharedPreferences.getInstance();

// Load production and release lists.
List<String>? searchHistoryListStaging =
storage.getStringList("priobike.routing.searchHistory.${Backend.staging.name}");
// Return on no key found.
if (searchHistoryListStaging == null) return;

List<String> searchHistoryListStagingNew =
storage.getStringList("priobike.routing.searchHistory.${Backend.staging.regionName}") ?? [];
// Concat both lists.
searchHistoryListStagingNew.addAll(searchHistoryListStaging);
// Store concatenated list.
await storage.setStringList(
"priobike.routing.searchHistory.${Backend.staging.regionName}", searchHistoryListStagingNew);
// Remove old list.
await storage.remove("priobike.routing.searchHistory.${Backend.staging.name}");
}

/// Adds test migration data for all backends.
static Future<void> addTestMigrationData() async {
final storage = await SharedPreferences.getInstance();

// Create old data for Staging.
List<Shortcut> stagingList = [
ShortcutLocation(
id: UniqueKey().toString(),
name: "Staging-Location-Test",
waypoint: Waypoint(51.038294, 13.703280, address: "Clara-Viebig-Straße 9"),
),
ShortcutRoute(
id: UniqueKey().toString(),
name: "Staging-Route-Test",
waypoints: [
Waypoint(51.038294, 13.703280, address: "Clara-Viebig-Straße 9"),
Waypoint(50.979067, 13.882596, address: "Elberadweg Heidenau"),
],
),
];

final jsonStrStaging = jsonEncode(stagingList.map((e) => e.toJson()).toList());

storage.setString("priobike.home.shortcuts.${Backend.staging.name}", jsonStrStaging);

// Create old data for Production.
List<Shortcut> productionList = [
ShortcutLocation(
id: UniqueKey().toString(),
name: "Production-Location-Test",
waypoint: Waypoint(53.5415701077766, 9.984275605794686, address: "Staging-test"),
),
ShortcutRoute(
id: UniqueKey().toString(),
name: "Production-Route-Test",
waypoints: [
Waypoint(53.560863, 9.990909, address: "Theodor-Heuss-Platz, Hamburg"),
Waypoint(53.564378, 9.978001, address: "Rentzelstraße 55, 20146 Hamburg"),
],
),
];

final jsonStrProduction = jsonEncode(productionList.map((e) => e.toJson()).toList());

storage.setString("priobike.home.shortcuts.${Backend.production.name}", jsonStrProduction);

// Create old data for Release.
List<Shortcut> releaseList = [
ShortcutLocation(
id: UniqueKey().toString(),
name: "Release-Location-Test",
waypoint: Waypoint(53.5415701077766, 9.984275605794686, address: "Staging-test"),
),
ShortcutRoute(
id: UniqueKey().toString(),
name: "Release-Route-Test",
waypoints: [
Waypoint(53.560863, 9.990909, address: "Theodor-Heuss-Platz, Hamburg"),
Waypoint(53.564378, 9.978001, address: "Rentzelstraße 55, 20146 Hamburg"),
],
),
];

final jsonStrRelease = jsonEncode(releaseList.map((e) => e.toJson()).toList());

storage.setString("priobike.home.shortcuts.${Backend.release.name}", jsonStrRelease);

// Create old search history data.
await storage.setStringList("priobike.routing.searchHistory.${Backend.staging.name}",
[json.encode(Waypoint(51.038294, 13.703280, address: "Clara-Viebig-Straße 9").toJSON())]);
await storage.setStringList("priobike.routing.searchHistory.${Backend.production.name}",
[json.encode(Waypoint(53.560863, 9.990909, address: "Theodor-Heuss-Platz, Hamburg").toJSON())]);
await storage.setStringList("priobike.routing.searchHistory.${Backend.release.name}",
[json.encode(Waypoint(53.560863, 9.990909, address: "Theodor-Heuss-Platz, Hamburg").toJSON())]);
}
}
Loading

0 comments on commit 9032edf

Please sign in to comment.