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.
		
		
		
		
		
			
		
			
				
	
	
		
			260 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			260 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Swift
		
	
| // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | |
| 
 | |
| import Foundation
 | |
| import Combine
 | |
| import Sodium
 | |
| 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()
 | |
|         
 | |
|         return SnodeAPI.getSwarm(for: userPublicKey)
 | |
|             .tryFlatMapWithRandomSnode { snode -> AnyPublisher<Void, Error> in
 | |
|                 CurrentUserPoller
 | |
|                     .poll(
 | |
|                         namespaces: [.configUserProfile],
 | |
|                         from: snode,
 | |
|                         for: userPublicKey,
 | |
|                         // Note: These values mean the received messages will be
 | |
|                         // processed immediately rather than async as part of a Job
 | |
|                         calledFromBackgroundPoller: true,
 | |
|                         isBackgroundPollValid: { true }
 | |
|                     )
 | |
|                     .tryFlatMap { receivedMessageTypes -> AnyPublisher<Void, Error> in
 | |
|                         // FIXME: Remove this entire 'tryFlatMap' once the updated user config has been released for long enough
 | |
|                         guard
 | |
|                             receivedMessageTypes.isEmpty,
 | |
|                             requestId == profileNameRetrievalIdentifier.wrappedValue
 | |
|                         else {
 | |
|                             return Just(())
 | |
|                                 .setFailureType(to: Error.self)
 | |
|                                 .eraseToAnyPublisher()
 | |
|                         }
 | |
|                         
 | |
|                         SNLog("Onboarding failed to retrieve user config, checking for legacy config")
 | |
|                         
 | |
|                         return CurrentUserPoller
 | |
|                             .poll(
 | |
|                                 namespaces: [.default],
 | |
|                                 from: snode,
 | |
|                                 for: userPublicKey,
 | |
|                                 // Note: These values mean the received messages will be
 | |
|                                 // processed immediately rather than async as part of a Job
 | |
|                                 calledFromBackgroundPoller: true,
 | |
|                                 isBackgroundPollValid: { true }
 | |
|                             )
 | |
|                             .tryMap { receivedMessageTypes -> Void in
 | |
|                                 guard
 | |
|                                     let message: ConfigurationMessage = receivedMessageTypes
 | |
|                                         .last(where: { $0 is ConfigurationMessage })
 | |
|                                         .asType(ConfigurationMessage.self),
 | |
|                                     let displayName: String = message.displayName,
 | |
|                                     requestId == profileNameRetrievalIdentifier.wrappedValue
 | |
|                                 else { return () }
 | |
|                                 
 | |
|                                 // Handle user profile changes
 | |
|                                 Storage.shared.write { db in
 | |
|                                     try ProfileManager.updateProfileIfNeeded(
 | |
|                                         db,
 | |
|                                         publicKey: userPublicKey,
 | |
|                                         name: displayName,
 | |
|                                         avatarUpdate: {
 | |
|                                             guard
 | |
|                                                 let profilePictureUrl: String = message.profilePictureUrl,
 | |
|                                                 let profileKey: Data = message.profileKey
 | |
|                                             else { return .none }
 | |
|                                             
 | |
|                                             return .updateTo(
 | |
|                                                 url: profilePictureUrl,
 | |
|                                                 key: profileKey,
 | |
|                                                 fileName: nil
 | |
|                                             )
 | |
|                                         }(),
 | |
|                                         sentTimestamp: TimeInterval((message.sentTimestamp ?? 0) / 1000),
 | |
|                                         calledFromConfigHandling: false,
 | |
|                                         using: dependencies
 | |
|                                     )
 | |
|                                 }
 | |
|                                 return ()
 | |
|                             }
 | |
|                             .eraseToAnyPublisher()
 | |
|                     }
 | |
|             }
 | |
|             .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 State {
 | |
|         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
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     enum Flow {
 | |
|         case register, recover, link
 | |
|         
 | |
|         /// 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() {
 | |
|             // Clear the in-memory state from SessionUtil
 | |
|             SessionUtil.clearMemoryState()
 | |
|             
 | |
|             // Clear any data which gets set during Onboarding
 | |
|             Storage.shared.write { db in
 | |
|                 db[.hasViewedSeed] = 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 }
 | |
|             
 | |
|             UserDefaults.standard[.hasSyncedInitialConfiguration] = false
 | |
|         }
 | |
|         
 | |
|         func preregister(with seed: Data, ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair) {
 | |
|             let x25519PublicKey = x25519KeyPair.hexEncodedPublicKey
 | |
|             
 | |
|             // Create the initial shared util state (won't have been created on
 | |
|             // launch due to lack of ed25519 key)
 | |
|             SessionUtil.loadState(
 | |
|                 userPublicKey: x25519PublicKey,
 | |
|                 ed25519SecretKey: ed25519KeyPair.secretKey
 | |
|             )
 | |
|             
 | |
|             // 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 || self == .link)
 | |
|                 
 | |
|                 // 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 the
 | |
|             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()
 | |
|             
 | |
|             // Now that we have registered get the Snode pool (just in case) - other non-blocking
 | |
|             // launch jobs will automatically be run because the app activation was triggered
 | |
|             GetSnodePoolJob.run()
 | |
|         }
 | |
|     }
 | |
| }
 |