Skip to content

Commit 158ba60

Browse files
committed
Merge branch 'trunk' of github.com:wordpress-mobile/WordPress-iOS into task/success-card-actions
2 parents 5e3d235 + 907532e commit 158ba60

File tree

10 files changed

+356
-48
lines changed

10 files changed

+356
-48
lines changed

WordPress/Classes/Services/JetpackNotificationMigrationService.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,16 @@ final class JetpackNotificationMigrationService: JetpackNotificationMigrationSer
2525

2626
var wordPressNotificationsEnabled: Bool {
2727
get {
28-
return remoteNotificationRegister.isRegisteredForRemoteNotifications
28+
/// UIApplication.shared.isRegisteredForRemoteNotifications should be always accessed from main thread
29+
if Thread.isMainThread {
30+
return remoteNotificationRegister.isRegisteredForRemoteNotifications
31+
} else {
32+
var isRegisteredForRemoteNotifications = false
33+
DispatchQueue.main.sync {
34+
isRegisteredForRemoteNotifications = remoteNotificationRegister.isRegisteredForRemoteNotifications
35+
}
36+
return isRegisteredForRemoteNotifications
37+
}
2938
}
3039

3140
set {
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import Foundation
22

3+
/// Factory was used to sync WP & JP UserDefaults. It was decided to do it as one-off instead.
4+
/// Instead of refactoring the call-sites, we simply return `standard` here. Although it looks
5+
/// redundant, it gives us more flexibility to go back to syncing or apply similar changes and effect is isolated.
6+
/// If it is evident that this kind of thing is no longer needed after migration is complete, we can remove this
7+
/// and update call-sites to call `standard` directly.
38
@objc
49
final class UserPersistentStoreFactory: NSObject {
510
static func instance() -> UserPersistentRepository {
6-
FeatureFlag.contentMigration.enabled ? UserPersistentStore.standard : UserDefaults.standard
11+
UserDefaults.standard
712
}
813

914
@objc
1015
static func userDefaultsInstance() -> UserDefaults {
11-
guard FeatureFlag.contentMigration.enabled, let defaults = UserDefaults(suiteName: WPAppGroupName) else {
12-
return UserDefaults.standard
13-
}
14-
15-
return defaults
16+
return UserDefaults.standard
1617
}
1718
}

WordPress/Classes/System/WordPressAppDelegate.swift

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -154,20 +154,9 @@ class WordPressAppDelegate: UIResponder, UIApplicationDelegate {
154154

155155
NotificationCenter.default.post(name: .applicationLaunchCompleted, object: nil)
156156

157-
copyToSharedDefaultsIfNeeded()
158157
return true
159158
}
160159

161-
private func copyToSharedDefaultsIfNeeded() {
162-
if !AppConfiguration.isJetpack && FeatureFlag.contentMigration.enabled && !UserPersistentStore.standard.isOneOffMigrationComplete {
163-
let dict = UserDefaults.standard.dictionaryRepresentation()
164-
for (key, value) in dict {
165-
UserPersistentStore.standard.set(value, forKey: key)
166-
}
167-
UserPersistentStore.standard.isOneOffMigrationComplete = true
168-
}
169-
}
170-
171160
func applicationWillTerminate(_ application: UIApplication) {
172161
DDLogInfo("\(self) \(#function)")
173162
}

WordPress/Classes/Utility/Blogging Prompts/PromptRemindersScheduler.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,12 @@ protocol LocalFileStore {
467467

468468
@discardableResult
469469
func save(contents: Data, at url: URL) -> Bool
470+
471+
func containerURL(forAppGroup appGroup: String) -> URL?
472+
473+
func removeItem(at url: URL) throws
474+
475+
func copyItem(at srcURL: URL, to dstURL: URL) throws
470476
}
471477

472478
extension LocalFileStore {
@@ -476,6 +482,10 @@ extension LocalFileStore {
476482
}
477483

478484
extension FileManager: LocalFileStore {
485+
func containerURL(forAppGroup appGroup: String) -> URL? {
486+
return containerURL(forSecurityApplicationGroupIdentifier: appGroup)
487+
}
488+
479489
func fileExists(at url: URL) -> Bool {
480490
return fileExists(atPath: url.path)
481491
}

WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ enum FeatureFlag: Int, CaseIterable, OverrideableFlag {
123123
case .newCoreDataContext:
124124
return true
125125
case .jetpackMigrationPreventDuplicateNotifications:
126-
return false
126+
return true
127127
case .jetpackFeaturesRemovalPhaseOne:
128128
return false
129129
case .jetpackFeaturesRemovalPhaseTwo:

WordPress/Classes/Utility/KeychainUtils.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,16 @@ class KeychainUtils: NSObject {
2222
try keychainUtils.storeUsername(username, andPassword: password, forServiceName: serviceName, accessGroup: destinationAccessGroup, updateExisting: updateExisting)
2323
}
2424
}
25+
26+
func password(for username: String, serviceName: String, accessGroup: String? = nil) throws -> String? {
27+
return try keychainUtils.getPasswordForUsername(username, andServiceName: serviceName, accessGroup: accessGroup)
28+
}
29+
30+
func store(username: String, password: String, serviceName: String, accessGroup: String? = nil, updateExisting: Bool) throws {
31+
return try keychainUtils.storeUsername(username,
32+
andPassword: password,
33+
forServiceName: serviceName,
34+
accessGroup: accessGroup,
35+
updateExisting: updateExisting)
36+
}
2537
}

WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsPinnedItemStore.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ protocol SiteStatsPinnable { /* not implemented */ }
55

66
final class SiteStatsPinnedItemStore {
77
private(set) lazy var items: [SiteStatsPinnable] = {
8-
let presentBloggingReminders = Feature.enabled(.bloggingReminders) && JetpackNotificationMigrationService.shared.shouldPresentNotifications()
8+
let presentBloggingReminders = Feature.enabled(.bloggingReminders) && jetpackNotificationMigrationService.shouldPresentNotifications()
99
return presentBloggingReminders ?
1010
[GrowAudienceCell.HintType.social,
1111
GrowAudienceCell.HintType.bloggingReminders,

WordPress/Jetpack/Classes/Utility/DataMigrator.swift

Lines changed: 165 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,36 @@
11
final class DataMigrator {
22

3+
/// `DefaultsWrapper` is used to single out a dictionary for the migration process.
4+
/// This way we can delete just the value for its key and leave the rest of shared defaults untouched.
5+
private struct DefaultsWrapper {
6+
static let dictKey = "defaults_staging_dictionary"
7+
let defaultsDict: [String: Any]
8+
}
9+
310
private let coreDataStack: CoreDataStack
411
private let backupLocation: URL?
512
private let keychainUtils: KeychainUtils
613
private let localDefaults: UserDefaults
714
private let sharedDefaults: UserDefaults?
15+
private let localFileStore: LocalFileStore
816

917
init(coreDataStack: CoreDataStack = ContextManager.sharedInstance(),
1018
backupLocation: URL? = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.org.wordpress")?.appendingPathComponent("WordPress.sqlite"),
1119
keychainUtils: KeychainUtils = KeychainUtils(),
1220
localDefaults: UserDefaults = UserDefaults.standard,
13-
sharedDefaults: UserDefaults? = UserDefaults(suiteName: WPAppGroupName)) {
21+
sharedDefaults: UserDefaults? = UserDefaults(suiteName: WPAppGroupName),
22+
localFileStore: LocalFileStore = FileManager.default) {
1423
self.coreDataStack = coreDataStack
1524
self.backupLocation = backupLocation
1625
self.keychainUtils = keychainUtils
1726
self.localDefaults = localDefaults
1827
self.sharedDefaults = sharedDefaults
28+
self.localFileStore = localFileStore
1929
}
2030

2131
enum DataMigratorError: Error {
2232
case localDraftsNotSynced
2333
case databaseCopyError
24-
case keychainError
2534
case sharedUserDefaultsNil
2635
}
2736

@@ -34,7 +43,7 @@ final class DataMigrator {
3443
completion?(.failure(.databaseCopyError))
3544
return
3645
}
37-
guard copyUserDefaults(from: localDefaults, to: sharedDefaults) else {
46+
guard populateSharedDefaults() else {
3847
completion?(.failure(.sharedUserDefaultsNil))
3948
return
4049
}
@@ -47,14 +56,29 @@ final class DataMigrator {
4756
completion?(.failure(.databaseCopyError))
4857
return
4958
}
50-
guard copyUserDefaults(from: sharedDefaults, to: localDefaults) else {
59+
guard populateFromSharedDefaults() else {
5160
completion?(.failure(.sharedUserDefaultsNil))
5261
return
5362
}
63+
64+
copyTodayWidgetDataToJetpack()
5465
BloggingRemindersScheduler.handleRemindersMigration()
5566
completion?(.success(()))
5667
}
5768

69+
/// Copies WP's Today Widget data (in Keychain and User Defaults) into JP.
70+
///
71+
/// Both WP and JP's extensions are already reading and storing data in the same location, but in case of Today Widget,
72+
/// the keys used for Keychain and User Defaults are differentiated to prevent one app overwriting the other.
73+
///
74+
/// Note: This method is not private for unit testing purposes.
75+
/// It requires time to properly mock the dependencies in `importData`.
76+
///
77+
func copyTodayWidgetDataToJetpack() {
78+
copyTodayWidgetKeychain()
79+
copyTodayWidgetUserDefaults()
80+
copyTodayWidgetCacheFiles()
81+
}
5882
}
5983

6084
// MARK: - Private Functions
@@ -96,26 +120,152 @@ private extension DataMigrator {
96120
return true
97121
}
98122

99-
func copyKeychain(from sourceAccessGroup: String?, to destinationAccessGroup: String?) -> Bool {
100-
do {
101-
try keychainUtils.copyKeychain(from: sourceAccessGroup, to: destinationAccessGroup)
102-
} catch {
103-
DDLogError("Error copying keychain: \(error)")
123+
func populateSharedDefaults() -> Bool {
124+
guard let sharedDefaults = sharedDefaults else {
104125
return false
105126
}
106127

128+
let data = localDefaults.dictionaryRepresentation()
129+
var temporaryDictionary: [String: Any] = [:]
130+
for (key, value) in data {
131+
temporaryDictionary[key] = value
132+
}
133+
sharedDefaults.set(temporaryDictionary, forKey: DefaultsWrapper.dictKey)
107134
return true
108135
}
109136

110-
func copyUserDefaults(from source: UserDefaults?, to destination: UserDefaults?) -> Bool {
111-
guard let source, let destination else {
137+
func populateFromSharedDefaults() -> Bool {
138+
guard let sharedDefaults = sharedDefaults,
139+
let temporaryDictionary = sharedDefaults.dictionary(forKey: DefaultsWrapper.dictKey) else {
112140
return false
113141
}
114-
let data = source.dictionaryRepresentation()
115-
for (key, value) in data {
116-
destination.set(value, forKey: key)
117-
}
118142

143+
for (key, value) in temporaryDictionary {
144+
localDefaults.set(value, forKey: key)
145+
}
146+
sharedDefaults.removeObject(forKey: DefaultsWrapper.dictKey)
119147
return true
120148
}
121149
}
150+
151+
// MARK: - Today Widget Extension Constants
152+
153+
private extension DataMigrator {
154+
155+
func copyTodayWidgetKeychain() {
156+
guard let authToken = try? keychainUtils.password(for: WPWidgetConstants.keychainTokenKey.rawValue,
157+
serviceName: WPWidgetConstants.keychainServiceName.rawValue,
158+
accessGroup: WPAppKeychainAccessGroup) else {
159+
return
160+
}
161+
162+
try? keychainUtils.store(username: WPWidgetConstants.keychainTokenKey.valueForJetpack(),
163+
password: authToken,
164+
serviceName: WPWidgetConstants.keychainServiceName.valueForJetpack(),
165+
updateExisting: true)
166+
}
167+
168+
func copyTodayWidgetUserDefaults() {
169+
guard let sharedDefaults else {
170+
return
171+
}
172+
173+
let userDefaultKeys: [WPWidgetConstants] = [
174+
.userDefaultsSiteIdKey,
175+
.userDefaultsLoggedInKey,
176+
.statsUserDefaultsSiteIdKey,
177+
.statsUserDefaultsSiteUrlKey,
178+
.statsUserDefaultsSiteNameKey,
179+
.statsUserDefaultsSiteTimeZoneKey
180+
]
181+
182+
userDefaultKeys.forEach { key in
183+
// go to the next key if there's nothing stored under the current key.
184+
guard let objectToMigrate = sharedDefaults.object(forKey: key.rawValue) else {
185+
return
186+
}
187+
188+
sharedDefaults.set(objectToMigrate, forKey: key.valueForJetpack())
189+
}
190+
}
191+
192+
func copyTodayWidgetCacheFiles() {
193+
let fileNames: [WPWidgetConstants] = [
194+
.todayFilename,
195+
.allTimeFilename,
196+
.thisWeekFilename,
197+
.statsTodayFilename,
198+
.statsThisWeekFilename,
199+
.statsAllTimeFilename
200+
]
201+
202+
fileNames.forEach { fileName in
203+
guard let sourceURL = localFileStore.containerURL(forAppGroup: WPAppGroupName)?.appendingPathComponent(fileName.rawValue),
204+
let targetURL = localFileStore.containerURL(forAppGroup: WPAppGroupName)?.appendingPathComponent(fileName.valueForJetpack()),
205+
localFileStore.fileExists(at: sourceURL) else {
206+
return
207+
}
208+
209+
if localFileStore.fileExists(at: targetURL) {
210+
try? localFileStore.removeItem(at: targetURL)
211+
}
212+
213+
try? localFileStore.copyItem(at: sourceURL, to: targetURL)
214+
}
215+
}
216+
217+
/// Keys relevant for migration, copied from WidgetConfiguration.
218+
///
219+
enum WPWidgetConstants: String {
220+
// Constants for Home Widget
221+
case keychainTokenKey = "OAuth2Token"
222+
case keychainServiceName = "TodayWidget"
223+
case userDefaultsSiteIdKey = "WordPressHomeWidgetsSiteId"
224+
case userDefaultsLoggedInKey = "WordPressHomeWidgetsLoggedIn"
225+
case todayFilename = "HomeWidgetTodayData.plist" // HomeWidgetTodayData
226+
case allTimeFilename = "HomeWidgetAllTimeData.plist" // HomeWidgetAllTimeData
227+
case thisWeekFilename = "HomeWidgetThisWeekData.plist" // HomeWidgetThisWeekData
228+
229+
// Constants for Stats Widget
230+
case statsUserDefaultsSiteIdKey = "WordPressTodayWidgetSiteId"
231+
case statsUserDefaultsSiteNameKey = "WordPressTodayWidgetSiteName"
232+
case statsUserDefaultsSiteUrlKey = "WordPressTodayWidgetSiteUrl"
233+
case statsUserDefaultsSiteTimeZoneKey = "WordPressTodayWidgetTimeZone"
234+
case statsTodayFilename = "TodayData.plist" // TodayWidgetStats
235+
case statsThisWeekFilename = "ThisWeekData.plist" // ThisWeekWidgetStats
236+
case statsAllTimeFilename = "AllTimeData.plist" // AllTimeWidgetStats
237+
238+
func valueForJetpack() -> String {
239+
switch self {
240+
case .keychainTokenKey:
241+
return "OAuth2Token"
242+
case .keychainServiceName:
243+
return "JetpackTodayWidget"
244+
case .userDefaultsSiteIdKey:
245+
return "JetpackHomeWidgetsSiteId"
246+
case .userDefaultsLoggedInKey:
247+
return "JetpackHomeWidgetsLoggedIn"
248+
case .todayFilename:
249+
return "JetpackHomeWidgetTodayData.plist"
250+
case .allTimeFilename:
251+
return "JetpackHomeWidgetAllTimeData.plist"
252+
case .thisWeekFilename:
253+
return "JetpackHomeWidgetThisWeekData.plist"
254+
case .statsUserDefaultsSiteIdKey:
255+
return "JetpackTodayWidgetSiteId"
256+
case .statsUserDefaultsSiteNameKey:
257+
return "JetpackTodayWidgetSiteName"
258+
case .statsUserDefaultsSiteUrlKey:
259+
return "JetpackTodayWidgetSiteUrl"
260+
case .statsUserDefaultsSiteTimeZoneKey:
261+
return "JetpackTodayWidgetTimeZone"
262+
case .statsTodayFilename:
263+
return "JetpackTodayData.plist"
264+
case .statsThisWeekFilename:
265+
return "JetpackThisWeekData.plist"
266+
case .statsAllTimeFilename:
267+
return "JetpackAllTimeData.plist"
268+
}
269+
}
270+
}
271+
}

0 commit comments

Comments
 (0)