mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			216 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			216 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Swift
		
	
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | 
						|
 | 
						|
import Foundation
 | 
						|
import Combine
 | 
						|
import GRDB
 | 
						|
import SessionUtilitiesKit
 | 
						|
import SessionMessagingKit
 | 
						|
import SessionSnodeKit
 | 
						|
 | 
						|
enum Onboarding {
 | 
						|
    private static let profileNameRetrievalIdentifier: Atomic<UUID?> = Atomic(nil)
 | 
						|
    private static let profileNameRetrievalPublisher: Atomic<AnyPublisher<String?, Error>?> = Atomic(nil)
 | 
						|
    public static var profileNamePublisher: AnyPublisher<String?, Error> {
 | 
						|
        guard let existingPublisher: AnyPublisher<String?, Error> = profileNameRetrievalPublisher.wrappedValue else {
 | 
						|
            return profileNameRetrievalPublisher.mutate { value in
 | 
						|
                let requestId: UUID = UUID()
 | 
						|
                let result: AnyPublisher<String?, Error> = createProfileNameRetrievalPublisher(requestId)
 | 
						|
                
 | 
						|
                value = result
 | 
						|
                profileNameRetrievalIdentifier.mutate { $0 = requestId }
 | 
						|
                return result
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        return existingPublisher
 | 
						|
    }
 | 
						|
    
 | 
						|
    private static func createProfileNameRetrievalPublisher(
 | 
						|
        _ requestId: UUID,
 | 
						|
        using dependencies: Dependencies = Dependencies()
 | 
						|
    ) -> AnyPublisher<String?, Error> {
 | 
						|
        let userPublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
 | 
						|
        
 | 
						|
        return CurrentUserPoller()
 | 
						|
            .poll(
 | 
						|
                namespaces: [.configUserProfile],
 | 
						|
                for: userPublicKey,
 | 
						|
                drainBehaviour: .alwaysRandom,
 | 
						|
                forceSynchronousProcessing: true,
 | 
						|
                using: dependencies
 | 
						|
            )
 | 
						|
            .map { _ -> String? in
 | 
						|
                guard requestId == profileNameRetrievalIdentifier.wrappedValue else { return nil }
 | 
						|
                
 | 
						|
                return Storage.shared.read { db in
 | 
						|
                    try Profile
 | 
						|
                        .filter(id: userPublicKey)
 | 
						|
                        .select(.name)
 | 
						|
                        .asRequest(of: String.self)
 | 
						|
                        .fetchOne(db)
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .shareReplay(1)
 | 
						|
            .eraseToAnyPublisher()
 | 
						|
    }
 | 
						|
    
 | 
						|
    enum SeedSource {
 | 
						|
        case qrCode
 | 
						|
        case mnemonic
 | 
						|
        
 | 
						|
        var genericErrorMessage: String {
 | 
						|
            switch self {
 | 
						|
                case .qrCode:
 | 
						|
                    "qrNotRecoveryPassword".localized()
 | 
						|
                case .mnemonic:
 | 
						|
                    "recoveryPasswordErrorMessageGeneric".localized()
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    enum State: CustomStringConvertible {
 | 
						|
        case newUser
 | 
						|
        case missingName
 | 
						|
        case completed
 | 
						|
        
 | 
						|
        static var current: State {
 | 
						|
            // If we have no identify information then the user needs to register
 | 
						|
            guard Identity.userExists() else { return .newUser }
 | 
						|
            
 | 
						|
            // If we have no display name then collect one (this can happen if the
 | 
						|
            // app crashed during onboarding which would leave the user in an invalid
 | 
						|
            // state with no display name)
 | 
						|
            guard !Profile.fetchOrCreateCurrentUser().name.isEmpty else { return .missingName }
 | 
						|
            
 | 
						|
            // Otherwise we have enough for a full user and can start the app
 | 
						|
            return .completed
 | 
						|
        }
 | 
						|
        
 | 
						|
        var description: String {
 | 
						|
            switch self {
 | 
						|
                case .newUser: return "New User"            // stringlint:disable
 | 
						|
                case .missingName: return "Missing Name"    // stringlint:disable
 | 
						|
                case .completed: return "Completed"         // stringlint:disable
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    enum Flow {
 | 
						|
        case register, recover
 | 
						|
        
 | 
						|
        /// If the user returns to an earlier screen during Onboarding we might need to clear out a partially created
 | 
						|
        /// account (eg. returning from the PN setting screen to the seed entry screen when linking a device)
 | 
						|
        func unregister(using dependencies: Dependencies) {
 | 
						|
            // Clear the in-memory state from LibSession
 | 
						|
            LibSession.clearMemoryState(using: dependencies)
 | 
						|
            
 | 
						|
            // Clear any data which gets set during Onboarding
 | 
						|
            Storage.shared.write { db in
 | 
						|
                db[.hasViewedSeed] = false
 | 
						|
                db[.hideRecoveryPasswordPermanently] = false
 | 
						|
                
 | 
						|
                try SessionThread.deleteAll(db)
 | 
						|
                try Profile.deleteAll(db)
 | 
						|
                try Contact.deleteAll(db)
 | 
						|
                try Identity.deleteAll(db)
 | 
						|
                try ConfigDump.deleteAll(db)
 | 
						|
                try SnodeReceivedMessageInfo.deleteAll(db)
 | 
						|
            }
 | 
						|
            
 | 
						|
            // Clear the profile name retrieve publisher
 | 
						|
            profileNameRetrievalIdentifier.mutate { $0 = nil }
 | 
						|
            profileNameRetrievalPublisher.mutate { $0 = nil }
 | 
						|
            
 | 
						|
            // Clear the cached 'encodedPublicKey' if needed
 | 
						|
            dependencies.caches.mutate(cache: .general) { $0.encodedPublicKey = nil }
 | 
						|
            
 | 
						|
            UserDefaults.standard[.hasSyncedInitialConfiguration] = false
 | 
						|
        }
 | 
						|
        
 | 
						|
        func preregister(with seed: Data, ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair, using dependencies: Dependencies) {
 | 
						|
            let x25519PublicKey = x25519KeyPair.hexEncodedPublicKey
 | 
						|
            
 | 
						|
            // Create the initial shared util state (won't have been created on
 | 
						|
            // launch due to lack of ed25519 key)
 | 
						|
            LibSession.loadState(
 | 
						|
                userPublicKey: x25519PublicKey,
 | 
						|
                ed25519SecretKey: ed25519KeyPair.secretKey,
 | 
						|
                using: dependencies
 | 
						|
            )
 | 
						|
            
 | 
						|
            // Store the user identity information
 | 
						|
            Storage.shared.write { db in
 | 
						|
                try Identity.store(
 | 
						|
                    db,
 | 
						|
                    seed: seed,
 | 
						|
                    ed25519KeyPair: ed25519KeyPair,
 | 
						|
                    x25519KeyPair: x25519KeyPair
 | 
						|
                )
 | 
						|
                
 | 
						|
                // No need to show the seed again if the user is restoring or linking
 | 
						|
                db[.hasViewedSeed] = (self == .recover)
 | 
						|
                
 | 
						|
                // Create a contact for the current user and set their approval/trusted statuses so
 | 
						|
                // they don't get weird behaviours
 | 
						|
                try Contact
 | 
						|
                    .fetchOrCreate(db, id: x25519PublicKey)
 | 
						|
                    .save(db)
 | 
						|
                try Contact
 | 
						|
                    .filter(id: x25519PublicKey)
 | 
						|
                    .updateAllAndConfig(
 | 
						|
                        db,
 | 
						|
                        Contact.Columns.isTrusted.set(to: true),    // Always trust the current user
 | 
						|
                        Contact.Columns.isApproved.set(to: true),
 | 
						|
                        Contact.Columns.didApproveMe.set(to: true)
 | 
						|
                    )
 | 
						|
 | 
						|
                /// Create the 'Note to Self' thread (not visible by default)
 | 
						|
                ///
 | 
						|
                /// **Note:** We need to explicitly `updateAllAndConfig` the `shouldBeVisible` value to `false`
 | 
						|
                /// otherwise it won't actually get synced correctly
 | 
						|
                try SessionThread
 | 
						|
                    .fetchOrCreate(db, id: x25519PublicKey, variant: .contact, shouldBeVisible: false)
 | 
						|
                
 | 
						|
                try SessionThread
 | 
						|
                    .filter(id: x25519PublicKey)
 | 
						|
                    .updateAllAndConfig(
 | 
						|
                        db,
 | 
						|
                        SessionThread.Columns.shouldBeVisible.set(to: false)
 | 
						|
                    )
 | 
						|
            }
 | 
						|
            
 | 
						|
            // Set hasSyncedInitialConfiguration to true so that when we hit the
 | 
						|
            // home screen a configuration sync is triggered (yes, the logic is a
 | 
						|
            // bit weird). This is needed so that if the user registers and
 | 
						|
            // immediately links a device, there'll be a configuration in their swarm.
 | 
						|
            UserDefaults.standard[.hasSyncedInitialConfiguration] = (self == .register)
 | 
						|
            
 | 
						|
            // Only continue if this isn't a new account
 | 
						|
            guard self != .register else { return }
 | 
						|
            
 | 
						|
            // Fetch any existing profile name
 | 
						|
            Onboarding.profileNamePublisher
 | 
						|
                .subscribe(on: DispatchQueue.global(qos: .userInitiated))
 | 
						|
                .sinkUntilComplete()
 | 
						|
        }
 | 
						|
        
 | 
						|
        func completeRegistration() {
 | 
						|
            // Set the `lastNameUpdate` to the current date, so that we don't overwrite
 | 
						|
            // what the user set in the display name step with whatever we find in their
 | 
						|
            // swarm (otherwise the user could enter a display name and have it immediately
 | 
						|
            // overwritten due to the config request running slow)
 | 
						|
            Storage.shared.write { db in
 | 
						|
                try Profile
 | 
						|
                    .filter(id: getUserHexEncodedPublicKey(db))
 | 
						|
                    .updateAllAndConfig(
 | 
						|
                        db,
 | 
						|
                        Profile.Columns.lastNameUpdate.set(to: Date().timeIntervalSince1970)
 | 
						|
                    )
 | 
						|
            }
 | 
						|
            
 | 
						|
            // Notify the app that registration is complete
 | 
						|
            Identity.didRegister()
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |