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.
263 lines
12 KiB
Swift
263 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) -> AnyPublisher<String?, Error> {
|
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
|
guard SessionUtil.userConfigsEnabled else {
|
|
return Just(nil)
|
|
.setFailureType(to: Error.self)
|
|
.eraseToAnyPublisher()
|
|
}
|
|
|
|
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
|
|
)
|
|
}
|
|
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 and sync push tokens
|
|
GetSnodePoolJob.run()
|
|
SyncPushTokensJob.run(uploadOnlyIfStale: false)
|
|
}
|
|
}
|
|
}
|