From 551ed202be7c20c4e8da0f11f94aabb4dc1add88 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 1 May 2023 20:36:52 +0400 Subject: [PATCH] ios: create address during onboarding (#2362) * ios: create address during onboarding * contact picker * email wip * send email w/t leaving app * fomatting * layout, texts * remove contact picker, add email button to address page * refactor * refactor --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/ios/Shared/Model/SimpleXAPI.swift | 2 +- .../Database/MigrateToAppGroupView.swift | 2 +- apps/ios/Shared/Views/Helpers/MailView.swift | 61 +++++ .../Views/Onboarding/CreateProfile.swift | 4 +- .../Onboarding/CreateSimpleXAddress.swift | 208 ++++++++++++++++++ .../Views/Onboarding/OnboardingView.swift | 6 +- .../Onboarding/SetNotificationsMode.swift | 10 +- .../Views/UserSettings/UserAddressView.swift | 38 +++- apps/ios/SimpleX.xcodeproj/project.pbxproj | 8 + 9 files changed, 329 insertions(+), 10 deletions(-) create mode 100644 apps/ios/Shared/Views/Helpers/MailView.swift create mode 100644 apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 3ecfa7079..70f772286 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1057,7 +1057,7 @@ func startChat(refreshInvitations: Bool = true) throws { } withAnimation { m.onboardingStage = m.onboardingStage == .step2_CreateProfile && m.users.count == 1 - ? .step3_SetNotificationsMode + ? .step3_CreateSimpleXAddress : .onboardingComplete } } diff --git a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift index de99d315e..d2c0d4d86 100644 --- a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift +++ b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift @@ -109,7 +109,7 @@ struct MigrateToAppGroupView: View { do { resetChatCtrl() try initializeChat(start: true) - chatModel.onboardingStage = .step3_SetNotificationsMode + chatModel.onboardingStage = .step4_SetNotificationsMode setV3DBMigration(.ready) } catch let error { dbContainerGroupDefault.set(.documents) diff --git a/apps/ios/Shared/Views/Helpers/MailView.swift b/apps/ios/Shared/Views/Helpers/MailView.swift new file mode 100644 index 000000000..75887a38c --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/MailView.swift @@ -0,0 +1,61 @@ +// +// MailView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 01.05.2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import UIKit +import MessageUI + +struct MailView: UIViewControllerRepresentable { + @Binding var isShowing: Bool + @Binding var result: Result? + var subject = "" + var messageBody = "" + + class Coordinator: NSObject, MFMailComposeViewControllerDelegate { + @Binding var isShowing: Bool + @Binding var result: Result? + + init(isShowing: Binding, + result: Binding?>) { + _isShowing = isShowing + _result = result + } + + func mailComposeController( + _ controller: MFMailComposeViewController, + didFinishWith result: MFMailComposeResult, + error: Error? + ) { + defer { + isShowing = false + } + if let error = error { + self.result = .failure(error) + return + } + self.result = .success(result) + } + } + + func makeCoordinator() -> Coordinator { + return Coordinator(isShowing: $isShowing, result: $result) + } + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> MFMailComposeViewController { + let vc = MFMailComposeViewController() + vc.setSubject(subject) + vc.setMessageBody(messageBody, isHTML: true) + vc.mailComposeDelegate = context.coordinator + return vc + } + + func updateUIViewController(_ uiViewController: MFMailComposeViewController, + context: UIViewControllerRepresentableContext) { + + } +} diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index d6b8c6b12..50048b502 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -34,7 +34,9 @@ struct CreateProfile: View { VStack(alignment: .leading) { Text("Create your profile") .font(.largeTitle) + .bold() .padding(.bottom, 4) + .frame(maxWidth: .infinity) Text("Your profile, contacts and delivered messages are stored on your device.") .padding(.bottom, 4) Text("The profile is only shared with your contacts.") @@ -122,7 +124,7 @@ struct CreateProfile: View { m.currentUser = try apiCreateActiveUser(profile) if m.users.isEmpty { try startChat() - withAnimation { m.onboardingStage = .step3_SetNotificationsMode } + withAnimation { m.onboardingStage = .step3_CreateSimpleXAddress } } else { dismiss() m.users = try listUsers() diff --git a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift new file mode 100644 index 000000000..2cc64c193 --- /dev/null +++ b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift @@ -0,0 +1,208 @@ +// +// CreateSimpleXAddress.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.04.2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import Contacts +import ContactsUI +import MessageUI +import SimpleXChat + +struct CreateSimpleXAddress: View { + @EnvironmentObject var m: ChatModel + @State private var progressIndicator = false + @State private var showMailView = false + @State private var mailViewResult: Result? = nil + + var body: some View { + GeometryReader { g in + ScrollView { + ZStack { + VStack(alignment: .leading) { + Text("SimpleX Address") + .font(.largeTitle) + .bold() + .frame(maxWidth: .infinity) + + Spacer() + + if let userAddress = m.userAddress { + QRCode(uri: userAddress.connReqContact) + .frame(maxHeight: g.size.width) + shareQRCodeButton(userAddress) + .frame(maxWidth: .infinity) + + Spacer() + + shareViaEmailButton(userAddress) + .frame(maxWidth: .infinity) + + Spacer() + + continueButton() + .padding(.bottom, 8) + .frame(maxWidth: .infinity) + } else { + createAddressButton() + .frame(maxWidth: .infinity) + + Spacer() + + skipButton() + .padding(.bottom, 56) + .frame(maxWidth: .infinity) + } + } + .frame(minHeight: g.size.height) + + if progressIndicator { + ProgressView().scaleEffect(2) + } + } + } + } + .frame(maxHeight: .infinity) + .padding() + } + + private func createAddressButton() -> some View { + VStack(spacing: 8) { + Button { + progressIndicator = true + Task { + do { + let connReqContact = try await apiCreateUserAddress() + DispatchQueue.main.async { + m.userAddress = UserContactLink(connReqContact: connReqContact) + } + if let u = try await apiSetProfileAddress(on: true) { + DispatchQueue.main.async { + m.updateUser(u) + } + } + await MainActor.run { progressIndicator = false } + } catch let error { + logger.error("CreateSimpleXAddress create address: \(responseError(error))") + await MainActor.run { progressIndicator = false } + let a = getErrorAlert(error, "Error creating address") + AlertManager.shared.showAlertMsg( + title: a.title, + message: a.message + ) + } + } + } label: { + Text("Create SimpleX address").font(.title) + } + Group { + Text("Your contacts in SimpleX will see it.\nYou can change it in Settings.") + } + .multilineTextAlignment(.center) + .font(.footnote) + .padding(.horizontal, 32) + } + } + + private func skipButton() -> some View { + VStack(spacing: 8) { + Button { + withAnimation { + m.onboardingStage = .step4_SetNotificationsMode + } + } label: { + HStack { + Text("Don't create address") + Image(systemName: "chevron.right") + } + } + Text("You can create it later").font(.footnote) + } + } + + private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View { + Button { + showShareSheet(items: [userAddress.connReqContact]) + } label: { + Label("Share", systemImage: "square.and.arrow.up") + } + } + + private func shareViaEmailButton(_ userAddress: UserContactLink) -> some View { + Button { + showMailView = true + } label: { + Label("Invite friends", systemImage: "envelope") + .font(.title2) + } + .sheet(isPresented: $showMailView) { + SendAddressMailView( + showMailView: $showMailView, + mailViewResult: $mailViewResult, + userAddress: userAddress + ) + .edgesIgnoringSafeArea(.bottom) + } + .onChange(of: mailViewResult == nil) { _ in + if let r = mailViewResult { + switch r { + case let .success(composeResult): + switch composeResult { + case .sent: + m.onboardingStage = .step4_SetNotificationsMode + default: () + } + case let .failure(error): + logger.error("CreateSimpleXAddress share via email: \(responseError(error))") + let a = getErrorAlert(error, "Error sending email") + AlertManager.shared.showAlertMsg( + title: a.title, + message: a.message + ) + } + mailViewResult = nil + } + } + } + + private func continueButton() -> some View { + Button { + withAnimation { + m.onboardingStage = .step4_SetNotificationsMode + } + } label: { + HStack { + Text("Continue") + Image(systemName: "greaterthan") + } + } + } +} + +struct SendAddressMailView: View { + @Binding var showMailView: Bool + @Binding var mailViewResult: Result? + var userAddress: UserContactLink + + var body: some View { + let messageBody = """ +

Hi!

+

Connect to me via SimpleX Chat

+ """ + MailView( + isShowing: self.$showMailView, + result: $mailViewResult, + subject: "Let's talk in SimpleX Chat", + messageBody: messageBody + ) + } +} + +struct CreateSimpleXAddress_Previews: PreviewProvider { + static var previews: some View { + CreateSimpleXAddress() + } +} diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift index 1ee8ecb6c..3d2c45dfa 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -15,7 +15,8 @@ struct OnboardingView: View { switch onboarding { case .step1_SimpleXInfo: SimpleXInfo(onboarding: true) case .step2_CreateProfile: CreateProfile() - case .step3_SetNotificationsMode: SetNotificationsMode() + case .step3_CreateSimpleXAddress: CreateSimpleXAddress() + case .step4_SetNotificationsMode: SetNotificationsMode() case .onboardingComplete: EmptyView() } } @@ -24,7 +25,8 @@ struct OnboardingView: View { enum OnboardingStage { case step1_SimpleXInfo case step2_CreateProfile - case step3_SetNotificationsMode + case step3_CreateSimpleXAddress + case step4_SetNotificationsMode case onboardingComplete } diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift index fa872e9c1..1a608f82c 100644 --- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -17,7 +17,10 @@ struct SetNotificationsMode: View { var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { - Text("Push notifications").font(.largeTitle) + Text("Push notifications") + .font(.largeTitle) + .bold() + .frame(maxWidth: .infinity) Text("Send notifications:") ForEach(NotificationsMode.values) { mode in @@ -62,9 +65,10 @@ struct SetNotificationsMode: View { m.notificationMode = mode } } catch let error { + let a = getErrorAlert(error, "Error enabling notifications") AlertManager.shared.showAlertMsg( - title: "Error enabling notifications", - message: "\(responseError(error))" + title: a.title, + message: a.message ) } } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 802cf4d4a..b8431e98e 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import MessageUI import SimpleXChat struct UserAddressView: View { @@ -17,6 +18,8 @@ struct UserAddressView: View { @State private var aas = AutoAcceptState() @State private var savedAAS = AutoAcceptState() @State private var ignoreShareViaProfileChange = false + @State private var showMailView = false + @State private var mailViewResult: Result? = nil @State private var alert: UserAddressAlert? @State private var showSaveDialogue = false @State private var progressIndicator = false @@ -189,6 +192,7 @@ struct UserAddressView: View { Section { QRCode(uri: userAddress.connReqContact) shareQRCodeButton(userAddress) + shareViaEmailButton(userAddress) shareWithContactsButton() autoAcceptToggle() learnMoreButton() @@ -240,9 +244,9 @@ struct UserAddressView: View { } } - private func shareQRCodeButton(_ userAdress: UserContactLink) -> some View { + private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View { Button { - showShareSheet(items: [userAdress.connReqContact]) + showShareSheet(items: [userAddress.connReqContact]) } label: { settingsRow("square.and.arrow.up") { Text("Share address") @@ -250,6 +254,36 @@ struct UserAddressView: View { } } + private func shareViaEmailButton(_ userAddress: UserContactLink) -> some View { + Button { + showMailView = true + } label: { + settingsRow("envelope") { + Text("Invite friends") + } + } + .sheet(isPresented: $showMailView) { + SendAddressMailView( + showMailView: $showMailView, + mailViewResult: $mailViewResult, + userAddress: userAddress + ) + .edgesIgnoringSafeArea(.bottom) + } + .onChange(of: mailViewResult == nil) { _ in + if let r = mailViewResult { + switch r { + case .success: () + case let .failure(error): + logger.error("UserAddressView share via email: \(responseError(error))") + let a = getErrorAlert(error, "Error sending email") + alert = .error(title: a.title, error: a.message) + } + mailViewResult = nil + } + } + } + private func autoAcceptToggle() -> some View { settingsRow("checkmark") { Toggle("Auto-accept", isOn: $aas.enable) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 3d666c757..5ed543453 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -151,6 +151,8 @@ 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; }; 6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; }; 6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; }; + 64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */; }; + 64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DCB29FFE3E800E3D48D /* MailView.swift */; }; 6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */; }; 644E72A629F18C00003534BE /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644E72A129F18C00003534BE /* libgmpxx.a */; }; 644E72A729F18C00003534BE /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644E72A229F18C00003534BE /* libffi.a */; }; @@ -418,6 +420,8 @@ 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = ""; }; 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = ""; }; 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = ""; }; + 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = ""; }; + 64466DCB29FFE3E800E3D48D /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = ""; }; 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = ""; }; 644E72A129F18C00003534BE /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 644E72A229F18C00003534BE /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; @@ -604,6 +608,7 @@ 18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */, 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */, 18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */, + 64466DCB29FFE3E800E3D48D /* MailView.swift */, ); path = Helpers; sourceTree = ""; @@ -668,6 +673,7 @@ 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */, 5CB0BA992827FD8800B3292C /* HowItWorks.swift */, 5CB0BA91282713FD00B3292C /* CreateProfile.swift */, + 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */, 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */, 5CBD285B29575B8E00EC2CF4 /* WhatsNewView.swift */, ); @@ -1085,6 +1091,7 @@ 5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */, 5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */, 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */, + 64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.swift in Sources */, 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */, 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */, 5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */, @@ -1129,6 +1136,7 @@ 5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */, 6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */, 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */, + 64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */, 5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */, 5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */, 6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */,