parse-community / Parse-Swift

The Swift SDK for Parse Platform (iOS, macOS, watchOS, tvOS, Linux, Android, Windows)
https://parseplatform.org
MIT License
308 stars 69 forks source link

feat: add migration from Obj-C SDK to Swift SDK #391

Closed cbaker6 closed 2 years ago

cbaker6 commented 2 years ago

New Pull Request Checklist

Issue Description

Currently there's no way to migrate from the Objective-C SDK to the Swift SDK. This enforces developers who convert to have to ask users to log into applications again after migration. More discussion in here.

Related issue: #n/a

Approach

Use the session token (assuming it's still valid) from an already logged in user in the Objective-C SDK Keychain to become/login into the Swift SDK. This allows a seamless migration/login without interrupting the application flow and does not modify the original Objective-C Keychain or the original session token. There is no need to have the Parse Objective-C SDK as a framework in your project as the Swift SDK doesn't need it to access the keychain since the keychain belongs to the respective app and can be access via the bundleId. The methods introduced to allow this are:

ParseUser

/**
     Logs in a `ParseUser` *asynchronously* using the session token from the Parse Objective-C SDK Keychain.
     Returns an instance of the successfully logged in `ParseUser`. The Parse Objective-C SDK Keychain is not
     modified in any way when calling this method; allowing developers to revert their applications back to the older
     SDK if desired.

     - parameter options: A set of header options sent to the server. Defaults to an empty set.
     - parameter callbackQueue: The queue to return to after completion. Default
     value of .main.
     - parameter completion: The block to execute when completed.
     It should have the following argument signature: `(Result<Self, ParseError>)`.
     - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
     desires a different policy, it should be inserted in `options`.
     - warning: When initializing the Swift SDK, `migratingFromObjcSDK` should be set to **false**
     when calling this method.
    */
    public func loginUsingObjCKeychain(options: API.Options = [],
                                       callbackQueue: DispatchQueue = .main,
                                       completion: @escaping (Result<Self, ParseError>) -> Void)

/**
     Logs in a `ParseUser` *asynchronously* using the session token from the Parse Objective-C SDK Keychain.
     Returns an instance of the successfully logged in `ParseUser`. The Parse Objective-C SDK Keychain is not
     modified in any way when calling this method; allowing developers to revert their applications back to the older
     SDK if desired.

     - parameter options: A set of header options sent to the server. Defaults to an empty set.
     - returns: Returns the logged in `ParseUser`.
     - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
     desires a different policy, it should be inserted in `options`.
     - warning: When initializing the Swift SDK, `migratingFromObjcSDK` should be set to **false**
     when calling this method.
    */
    @discardableResult func loginUsingObjCKeychain(options: API.Options = []) async throws -> Self

/**
     Logs in a `ParseUser` *asynchronously* using the session token from the Parse Objective-C SDK Keychain.
     Publishes an instance of the successfully logged in `ParseUser`. The Parse Objective-C SDK Keychain is not
     modified in any way when calling this method; allowing developers to revert their applications back to the older
     SDK if desired.

     - parameter options: A set of header options sent to the server. Defaults to an empty set.
     - returns: A publisher that eventually produces a single value and then finishes or fails.
     - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
     desires a different policy, it should be inserted in `options`.
     - warning: When initializing the Swift SDK, `migratingFromObjcSDK` should be set to **false**
     when calling this method.
    */
    func loginUsingObjCKeychainPublisher(options: API.Options = []) -> Future<Self, ParseError>

ParseInstallation

/**
     Migrates the `ParseInstallation` *asynchronously* from the Objective-C SDK Keychain.

     - parameter copyEntireInstallation: When **true**, copies the
     entire `ParseInstallation` from the Objective-C SDK Keychain to the Swift SDK. When
     **false**, only the `channels` and `deviceToken` are copied from the Objective-C
     SDK Keychain; resulting in a new `ParseInstallation` for original `sessionToken`.
     Defaults to **true**.
     - parameter options: A set of header options sent to the server. Defaults to an empty set.
     - parameter callbackQueue: The queue to return to after completion. Default value of .main.
     - parameter completion: The block to execute.
     It should have the following argument signature: `(Result<Self, ParseError>)`.
     - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
     desires a different policy, it should be inserted in `options`.
    */
    func migrateFromObjCKeychain(copyEntireInstallation: Bool = true,
                                 options: API.Options = [],
                                 callbackQueue: DispatchQueue = .main,
                                 completion: @escaping (Result<Self, ParseError>) -> Void)

/**
     Deletes the Objective-C Keychain along with the Objective-C `ParseInstallation`
     from the Parse Server *asynchronously* and executes the given callback block.

     - parameter options: A set of header options sent to the server. Defaults to an empty set.
     - parameter callbackQueue: The queue to return to after completion. Default
     value of .main.
     - parameter completion: The block to execute when completed.
     It should have the following argument signature: `(Result<Void, ParseError>)`.
     - warning: It is recommended to only use this method after a succesfful migration. Calling this
     method will destroy the entire Objective-C Keychain and `ParseInstallation` on the Parse
     Server.
     - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
     desires a different policy, it should be inserted in `options`.
    */
    func deleteObjCKeychain(options: API.Options = [],
                            callbackQueue: DispatchQueue = .main,
                            completion: @escaping (Result<Void, ParseError>) -> Void)

/**
     Migrates the `ParseInstallation` *asynchronously* from the Objective-C SDK Keychain.

     - parameter copyEntireInstallation: When **true**, copies the
     entire `ParseInstallation` from the Objective-C SDK Keychain to the Swift SDK. When
     **false**, only the `channels` and `deviceToken` are copied from the Objective-C
     SDK Keychain; resulting in a new `ParseInstallation` for original `sessionToken`.
     Defaults to **true**.
     - parameter options: A set of header options sent to the server. Defaults to an empty set.
     - returns: Returns saved `ParseInstallation`.
     - throws: An error of type `ParseError`.
     - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
     desires a different policy, it should be inserted in `options`.
    */
    @discardableResult func migrateFromObjCKeychain(copyEntireInstallation: Bool = true,
                                                    deleteObjectiveCKeychain: Bool = false,
                                                    options: API.Options = []) async throws -> Self

/**
     Deletes the Objective-C Keychain along with the Objective-C `ParseInstallation`
     from the Parse Server *asynchronously*.

     - parameter options: A set of header options sent to the server. Defaults to an empty set.
     - returns: Returns saved `ParseInstallation`.
     - throws: An error of type `ParseError`.
     - warning: It is recommended to only use this method after a succesfful migration. Calling this
     method will destroy the entire Objective-C Keychain and `ParseInstallation` on the Parse
     Server.
    */
    func deleteObjCKeychain(options: API.Options = []) async throws

/**
     Migrates the `ParseInstallation` *asynchronously* from the Objective-C SDK Keychain
     and publishes when complete.

     - parameter copyEntireInstallation: When **true**, copies the
     entire `ParseInstallation` from the Objective-C SDK Keychain to the Swift SDK. When
     **false**, only the `channels` and `deviceToken` are copied from the Objective-C
     SDK Keychain; resulting in a new `ParseInstallation` for original `sessionToken`.
     Defaults to **true**.
     - parameter options: A set of header options sent to the server. Defaults to an empty set.
     - returns: A publisher that eventually produces a single value and then finishes or fails.
     - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
     desires a different policy, it should be inserted in `options`.
    */
    func migrateFromObjCKeychainPublisher(copyEntireInstallation: Bool = true,
                                          options: API.Options = []) -> Future<Self, ParseError>

/**
     Deletes the Objective-C Keychain along with the Objective-C `ParseInstallation`
     from the Parse Server *asynchronously* and publishes when complete.

     - parameter options: A set of header options sent to the server. Defaults to an empty set.
     - returns: A publisher that eventually produces a single value and then finishes or fails.
     - warning: It is recommended to only use this method after a succesfful migration. Calling this
     method will destroy the entire Objective-C Keychain and `ParseInstallation` on the Parse
     Server.
    */
    func deleteObjCKeychainPublisher(options: API.Options = []) -> Future<Void, ParseError>

Note that logging in via the linked methods is the proper way to migrate to the Swift SDK as the SDK's themselves are entirely different and the Swift SDK should hydrate the _User and _Installation ParseObject's from the server instead of relying on the Objective-C SDK.

Developers should also ensure the latest PFUser and PFInstallation is saved to their Parse Server before migrating a user using these new methods. After initializing the SDK, migration should look something like:

struct User: ParseUser {
    //: These are required by `ParseObject`.
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?
    var originalData: Data?

    //: These are required by `ParseUser`.
    var username: String?
    var email: String?
    var emailVerified: Bool?
    var password: String?
    var authData: [String: [String: String]?]?

    //: Your custom keys.
    var customKey: String?

    //: Implement your own version of merge
    func merge(with object: Self) throws -> Self {
        var updated = try mergeParse(with: object)
        if updated.shouldRestoreKey(\.customKey,
                                     original: object) {
            updated.customKey = object.customKey
        }
        return updated
    }
}

struct Installation: ParseInstallation {
    //: These are required by `ParseObject`.
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?
    var originalData: Data?

    //: These are required by `ParseInstallation`.
    var installationId: String?
    var deviceType: String?
    var deviceToken: String?
    var badge: Int?
    var timeZone: String?
    var channels: [String]?
    var appName: String?
    var appIdentifier: String?
    var appVersion: String?
    var parseVersion: String?
    var localeIdentifier: String?

    //: Your custom keys
    var customKey: String?

    //: Implement your own version of merge
    func merge(with object: Self) throws -> Self {
        var updated = try mergeParse(with: object)
        if updated.shouldRestoreKey(\.customKey,
                                     original: object) {
            updated.customKey = object.customKey
        }
        return updated
    }
}

// Initialize Swift SDK in the same place you use to initialize the Objective-C SDK
// ....

// Place this code in the entry point of your app where you typically check if the Parse User is logged in
if User.current == nil {
  do {
    // Automatically migrate the user
    let migratedUser = try await User.loginUsingObjCKeychain()
    // Automatically migrate the installation
    let migratedInstallation = try await Installation.migrateFromObjCKeychain()

    // Sometime after you verified Migrations were successful, can delete old Keychain
    // Note, you don't need to delete the Keychain.
    let migratedInstallation = try await Installation.deleteObjCKeychain()
  } catch {
    print("Migration unsuccessful: \(error.localizedDescription)")
  }
}

TODOs before merging

parse-github-assistant[bot] commented 2 years ago

Thanks for opening this pull request!

codecov[bot] commented 2 years ago

Codecov Report

Merging #391 (b62ee9b) into main (675168e) will decrease coverage by 0.06%. The diff coverage is 75.23%.

@@            Coverage Diff             @@
##             main     #391      +/-   ##
==========================================
- Coverage   89.17%   89.10%   -0.07%     
==========================================
  Files         156      156              
  Lines       14553    14778     +225     
==========================================
+ Hits        12977    13168     +191     
- Misses       1576     1610      +34     
Impacted Files Coverage Δ
Sources/ParseSwift/API/Responses.swift 95.00% <0.00%> (ø)
...thentication/3rd Party/ParseApple/ParseApple.swift 86.25% <0.00%> (ø)
...Swift/Authentication/Internal/ParseAnonymous.swift 100.00% <ø> (ø)
Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift 64.70% <0.00%> (ø)
Sources/ParseSwift/LiveQuery/Subscription.swift 97.87% <0.00%> (ø)
...es/ParseSwift/LiveQuery/SubscriptionCallback.swift 70.45% <0.00%> (ø)
Sources/ParseSwift/Protocols/Objectable.swift 92.10% <0.00%> (ø)
...ources/ParseSwift/Protocols/ParseCloud+async.swift 100.00% <ø> (ø)
...rces/ParseSwift/Protocols/ParseCloud+combine.swift 100.00% <ø> (ø)
Sources/ParseSwift/Protocols/ParseCloud.swift 100.00% <ø> (ø)
... and 34 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

mtrezza commented 2 years ago

The Swift SDK will also create a new _Installation with a new installationId as well.

What is the rationale behind this? Why isn't it updating the existing installation? Technically the app isn't newly installed, it's updated which wouldn't create a new installation in other Parse SDKs.

the Swift SDK should hydrate the _User ParseObject from the server instead of relying on the Objective-C SDK.

Could you explain?

cbaker6 commented 2 years ago

What is the rationale behind this? Why isn't it updating the existing installation? Technically the app isn't newly installed, it's updated which wouldn't create a new installation in other Parse SDKs.

The server doesn’t let an app login with the same session token and same installation using “become” method. The installationId has to be different when leveraging the same sessionToken. The server is designed that way.

Could you explain?

I’ve stated the SDKs are completely different, https://github.com/parse-community/Parse-Swift/pull/391#issue-1342325742 in which they are, classes/structs, the way they encode/decode, etc. The Swift SDK knows how to speak to the server, not the Objective-C SDK or any other SDK.

mtrezza commented 2 years ago

The server doesn’t let an app login with the same session token and same installation using “become” method.

Why can't the Swift SDK migrate an existing ObjC SDK session token without using become? I assume that's what you mean by "hydrate" the ParseUser. The token has already been verified, because it has been stored previously by the ObjC SDK, so what verify it again with Parse Server?

cbaker6 commented 2 years ago

Why can't the Swift SDK migrate an existing ObjC SDK session token without using become?

The Swift SDK can get the sessionToken, which is already handled by the methods in this PR. The problem isn't the sessionToken, the problem is PFUser->ParseUser and PFInstallation->ParseInstallation.

I assume that's what you mean by "hydrate" the ParseUser. The token has already been verified, because it has been stored previously by the ObjC SDK, so what verify it again with Parse Server?

I've answered this in the comment above. The developer will want to access ParseUser.current and ParseInstallation.current the same way they access PFUser.current and PFInstallation.current. To do this properly, the developer needs to:

  1. Provide the proper structs on the client that conform to ParseUser and ParseInstallation
  2. Ensure all of the property labels and types match the structs in 1 so they can be decoded (hydrated) from the server
  3. The methods in this PR will then handle storing the aforementioned ParseObject's the way the Swift SDK needs them to be stored and the developer can convert the rest of their code by observing similar methods in the Playgrounds without disrupting the user.