Skip to content

Commit 90c17f5

Browse files
authored
Merge pull request #19710 from wordpress-mobile/feature/cmf-remove-export-flag-after-import
Jetpack Migration: Provide a path for JP to trigger data export in WP
2 parents 43e56ac + 524c371 commit 90c17f5

File tree

11 files changed

+354
-72
lines changed

11 files changed

+354
-72
lines changed

WordPress/Classes/System/WordPressAppDelegate+openURL.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ import AutomatticTracks
2020
return true
2121
}
2222

23+
/// WordPress only. Handle deeplink from JP that requests data export.
24+
let wordPressExportRouter = MigrationDeepLinkRouter(urlForScheme: URL(string: AppScheme.wordpressMigrationV1.rawValue),
25+
routes: [WordPressExportRoute()])
26+
if AppConfiguration.isWordPress,
27+
wordPressExportRouter.canHandle(url: url) {
28+
wordPressExportRouter.handle(url: url)
29+
return true
30+
}
31+
2332
if url.scheme == JetpackNotificationMigrationService.wordPressScheme {
2433
return JetpackNotificationMigrationService.shared.handleNotificationMigrationOnWordPress()
2534
}

WordPress/Classes/System/WordPressAppDelegate.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,13 @@ class WordPressAppDelegate: UIResponder, UIApplicationDelegate {
197197
updateFeatureFlags()
198198
updateRemoteConfig()
199199

200-
#if JETPACK
200+
#if JETPACK
201+
// JetpackWindowManager is only available in the Jetpack target.
201202
if let windowManager = windowManager as? JetpackWindowManager,
202203
windowManager.shouldImportMigrationData {
203-
windowManager.importAndShowMigrationContent(nil, failureCompletion: nil)
204+
windowManager.importAndShowMigrationContent()
204205
}
205-
#endif
206+
#endif
206207
}
207208

208209
func applicationWillResignActive(_ application: UIApplication) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/// A router that specifically handles deeplinks.
2+
/// Note that the capability of this router is very limited; it can only handle up to one path component (e.g.: `wordpress://intent`).
3+
///
4+
/// This is meant to be used during the WP->JP migratory period. Once we decide to move on from this phase, this class may be removed.
5+
///
6+
struct MigrationDeepLinkRouter: LinkRouter {
7+
8+
let routes: [Route]
9+
10+
/// when this is set, the router ensures that the URL has a scheme that matches this value.
11+
private var scheme: String? = nil
12+
13+
init(routes: [Route]) {
14+
self.routes = routes
15+
}
16+
17+
init(scheme: String?, routes: [Route]) {
18+
self.init(routes: routes)
19+
self.scheme = scheme
20+
}
21+
22+
init(urlForScheme: URL?, routes: [Route]) {
23+
self.init(scheme: urlForScheme?.scheme, routes: routes)
24+
}
25+
26+
func canHandle(url: URL) -> Bool {
27+
// if the scheme is set, check if the URL fulfills the requirement.
28+
if let scheme, url.scheme != scheme {
29+
return false
30+
}
31+
32+
/// deeplinks have their paths start at `host`, unlike universal links.
33+
/// e.g. wordpress://intent -> "intent" is the URL's host.
34+
///
35+
/// Ensure that the deeplink URL has a "host" that we can run against the `routes`' path.
36+
guard let deepLinkPath = url.host else {
37+
return false
38+
}
39+
40+
return routes
41+
.map { $0.path.removingPrefix("/") }
42+
.contains { $0 == deepLinkPath }
43+
}
44+
45+
func handle(url: URL, shouldTrack track: Bool = false, source: DeepLinkSource? = nil) {
46+
guard let deepLinkPath = url.host,
47+
let route = routes.filter({ $0.path.removingPrefix("/") == deepLinkPath }).first else {
48+
return
49+
}
50+
51+
// there's no need to pass any arguments or parameters since most of the migration deeplink routes are standalone.
52+
route.action.perform([:], source: nil, router: self)
53+
}
54+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/// Triggers the data export process on WordPress.
2+
///
3+
/// Note: this is only meant to be used in WordPress!
4+
///
5+
struct WordPressExportRoute: Route {
6+
let path = "/export-213"
7+
let section: DeepLinkSection? = nil
8+
var action: NavigationAction {
9+
return self
10+
}
11+
}
12+
13+
extension WordPressExportRoute: NavigationAction {
14+
func perform(_ values: [String: String], source: UIViewController?, router: LinkRouter) {
15+
guard AppConfiguration.isWordPress else {
16+
return
17+
}
18+
19+
ContentMigrationCoordinator.shared.startAndDo { _ in
20+
// Regardless of the result, redirect the user back to Jetpack.
21+
let jetpackUrl: URL? = {
22+
var components = URLComponents()
23+
components.scheme = JetpackNotificationMigrationService.jetpackScheme
24+
return components.url
25+
}()
26+
27+
guard let url = jetpackUrl,
28+
UIApplication.shared.canOpenURL(url) else {
29+
DDLogError("WordPressExportRoute: Cannot redirect back to the Jetpack app.")
30+
return
31+
}
32+
33+
UIApplication.shared.open(url)
34+
}
35+
}
36+
}

WordPress/Jetpack/Classes/System/JetpackWindowManager.swift

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class JetpackWindowManager: WindowManager {
66
private var cancellable: AnyCancellable?
77

88
/// Migration events tracking
9-
private let migrationTacker = MigrationAnalyticsTracker()
9+
private let migrationTracker = MigrationAnalyticsTracker()
1010

1111
var shouldImportMigrationData: Bool {
1212
return !AccountHelper.isLoggedIn && !UserPersistentStoreFactory.instance().isJPContentImportComplete
@@ -20,17 +20,8 @@ class JetpackWindowManager: WindowManager {
2020
}
2121

2222
guard AccountHelper.isLoggedIn else {
23-
let shouldImportMigrationData = shouldImportMigrationData
24-
self.migrationTacker.trackContentImportEligibility(eligible: shouldImportMigrationData)
25-
26-
if shouldImportMigrationData {
27-
importAndShowMigrationContent(blog) { [weak self] in
28-
self?.showSignInUI()
29-
}
30-
} else {
31-
showSignInUI()
32-
}
33-
23+
self.migrationTracker.trackContentImportEligibility(eligible: shouldImportMigrationData)
24+
shouldImportMigrationData ? importAndShowMigrationContent(blog) : showSignInUI()
3425
return
3526
}
3627

@@ -39,34 +30,49 @@ class JetpackWindowManager: WindowManager {
3930
AccountHelper.logOutDefaultWordPressComAccount()
4031
}
4132

42-
func importAndShowMigrationContent(_ blog: Blog?, failureCompletion: (() -> ())?) {
33+
func importAndShowMigrationContent(_ blog: Blog? = nil) {
34+
self.migrationTracker.trackWordPressMigrationEligibility()
35+
4336
DataMigrator().importData() { [weak self] result in
4437
guard let self else {
4538
return
4639
}
4740

4841
switch result {
4942
case .success:
50-
self.migrationTacker.trackContentImportSucceeded()
43+
self.migrationTracker.trackContentImportSucceeded()
5144
UserPersistentStoreFactory.instance().isJPContentImportComplete = true
5245
NotificationCenter.default.post(name: .WPAccountDefaultWordPressComAccountChanged, object: self)
5346
self.showMigrationUIIfNeeded(blog)
5447
self.sendMigrationEmail()
5548
case .failure(let error):
56-
self.migrationTacker.trackContentImportFailed(reason: error.localizedDescription)
57-
failureCompletion?()
49+
self.migrationTracker.trackContentImportFailed(reason: error.localizedDescription)
50+
self.handleMigrationFailure(error)
5851
}
5952
}
6053
}
54+
}
55+
56+
// MARK: - Private Helpers
57+
58+
private extension JetpackWindowManager {
59+
60+
var shouldShowMigrationUI: Bool {
61+
return FeatureFlag.contentMigration.enabled && AccountHelper.isLoggedIn
62+
}
6163

62-
private func sendMigrationEmail() {
64+
var isCompatibleWordPressAppPresent: Bool {
65+
MigrationAppDetection.getWordPressInstallationState() == .wordPressInstalledAndMigratable
66+
}
67+
68+
func sendMigrationEmail() {
6369
Task {
6470
let service = try? MigrationEmailService()
6571
try? await service?.sendMigrationEmail()
6672
}
6773
}
6874

69-
private func showMigrationUIIfNeeded(_ blog: Blog?) {
75+
func showMigrationUIIfNeeded(_ blog: Blog?) {
7076
guard shouldShowMigrationUI else {
7177
return
7278
}
@@ -83,12 +89,41 @@ class JetpackWindowManager: WindowManager {
8389
self.show(container.makeInitialViewController())
8490
}
8591

86-
private func switchToAppUI(for blog: Blog?) {
92+
func switchToAppUI(for blog: Blog?) {
8793
cancellable = nil
8894
showAppUI(for: blog)
8995
}
9096

91-
private var shouldShowMigrationUI: Bool {
92-
return FeatureFlag.contentMigration.enabled && AccountHelper.isLoggedIn
97+
/// Shown when the WordPress pre-flight process hasn't ran, but WordPress is installed.
98+
/// Note: We don't know if the user has ever logged into WordPress at this point, only
99+
/// that they have a version compatible with migrating.
100+
/// - Parameter schemeUrl: Deep link URL used to open the WordPress app
101+
func showLoadWordPressUI(schemeUrl: URL) {
102+
let actions = MigrationLoadWordPressViewModel.Actions()
103+
let loadWordPressViewModel = MigrationLoadWordPressViewModel(actions: actions)
104+
let loadWordPressViewController = MigrationLoadWordPressViewController(viewModel: loadWordPressViewModel)
105+
actions.primary = {
106+
UIApplication.shared.open(schemeUrl)
107+
}
108+
actions.secondary = { [weak self] in
109+
loadWordPressViewController.dismiss(animated: true) {
110+
self?.showSignInUI()
111+
}
112+
}
113+
self.show(loadWordPressViewController)
114+
}
115+
116+
func handleMigrationFailure(_ error: DataMigrationError) {
117+
guard
118+
case .dataNotReadyToImport = error,
119+
isCompatibleWordPressAppPresent,
120+
let schemeUrl = URL(string: "\(AppScheme.wordpressMigrationV1.rawValue)\(WordPressExportRoute().path.removingPrefix("/"))")
121+
else {
122+
showSignInUI()
123+
return
124+
}
125+
126+
/// WordPress is a compatible version for migrations, but needs to be loaded to prepare the data
127+
showLoadWordPressUI(schemeUrl: schemeUrl)
93128
}
94129
}

WordPress/Jetpack/Classes/Utility/DataMigrator.swift

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,18 @@ protocol ContentDataMigrating {
66
enum DataMigrationError: LocalizedError {
77
case databaseCopyError
88
case sharedUserDefaultsNil
9+
case dataNotReadyToImport
910

1011
var errorDescription: String? {
1112
switch self {
1213
case .databaseCopyError: return "The database couldn't be copied from shared directory"
1314
case .sharedUserDefaultsNil: return "Shared user defaults not found"
15+
case .dataNotReadyToImport: return "The data wasn't ready to import"
1416
}
1517
}
1618
}
1719

1820
final class DataMigrator {
19-
20-
/// `DefaultsWrapper` is used to single out a dictionary for the migration process.
21-
/// This way we can delete just the value for its key and leave the rest of shared defaults untouched.
22-
private struct DefaultsWrapper {
23-
static let dictKey = "defaults_staging_dictionary"
24-
let defaultsDict: [String: Any]
25-
}
26-
2721
private let coreDataStack: CoreDataStack
2822
private let backupLocation: URL?
2923
private let keychainUtils: KeychainUtils
@@ -57,14 +51,27 @@ extension DataMigrator: ContentDataMigrating {
5751
return
5852
}
5953
BloggingRemindersScheduler.handleRemindersMigration()
54+
55+
isDataReadyToMigrate = true
56+
6057
completion?(.success(()))
6158
}
6259

6360
func importData(completion: ((Result<Void, DataMigrationError>) -> Void)? = nil) {
61+
guard isDataReadyToMigrate else {
62+
completion?(.failure(.dataNotReadyToImport))
63+
return
64+
}
65+
6466
guard let backupLocation, restoreDatabase(from: backupLocation) else {
6567
completion?(.failure(.databaseCopyError))
6668
return
6769
}
70+
71+
/// Upon successful database restoration, the backup files in the App Group will be deleted.
72+
/// This means that the exported data is no longer complete when the user attempts another migration.
73+
isDataReadyToMigrate = false
74+
6875
guard populateFromSharedDefaults() else {
6976
completion?(.failure(.sharedUserDefaultsNil))
7077
return
@@ -81,6 +88,23 @@ extension DataMigrator: ContentDataMigrating {
8188
// MARK: - Private Functions
8289

8390
private extension DataMigrator {
91+
/// `DefaultsWrapper` is used to single out a dictionary for the migration process.
92+
/// This way we can delete just the value for its key and leave the rest of shared defaults untouched.
93+
struct DefaultsWrapper {
94+
static let dictKey = "defaults_staging_dictionary"
95+
let defaultsDict: [String: Any]
96+
}
97+
98+
/// Convenience wrapper to check whether the export data is ready to be imported.
99+
/// The value is stored in the App Group space so it is accessible from both apps.
100+
var isDataReadyToMigrate: Bool {
101+
get {
102+
sharedDefaults?.bool(forKey: .dataReadyToMigrateKey) ?? false
103+
}
104+
set {
105+
sharedDefaults?.set(newValue, forKey: .dataReadyToMigrateKey)
106+
}
107+
}
84108

85109
func copyDatabase(to destination: URL) -> Bool {
86110
do {
@@ -129,3 +153,7 @@ private extension DataMigrator {
129153
return true
130154
}
131155
}
156+
157+
private extension String {
158+
static let dataReadyToMigrateKey = "wp_data_migration_ready"
159+
}

WordPress/Jetpack/Classes/ViewRelated/WordPress-to-Jetpack Migration/Common/MigratableStateTracker.swift

Lines changed: 0 additions & 36 deletions
This file was deleted.

0 commit comments

Comments
 (0)