From 2bb5c24b21e1ad9048f91f0a8da6d6c36f11157e Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 19 Jan 2023 18:26:28 +0800 Subject: [PATCH] Allow setting self-host backends from the iOS app --- .../Sources/App/Views/DebugMenuView.swift | 2 +- .../App/Views/SelfHostSettingsView.swift | 86 +++++++++++++++++++ .../Sources/App/Views/WelcomeView.swift | 20 +++++ .../Sources/Models/AppEnvironment.swift | 35 ++++++-- 4 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/SelfHostSettingsView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/DebugMenuView.swift b/apple/OmnivoreKit/Sources/App/Views/DebugMenuView.swift index abff9974b..32da93f6c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/DebugMenuView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/DebugMenuView.swift @@ -8,7 +8,7 @@ struct DebugMenuView: View { @EnvironmentObject var dataService: DataService @Binding var selectedEnvironment: AppEnvironment - let appEnvironments: [AppEnvironment] = [.local, .demo, .dev, .prod] + let appEnvironments: [AppEnvironment] = [.local, .demo, .prod] var body: some View { VStack { diff --git a/apple/OmnivoreKit/Sources/App/Views/SelfHostSettingsView.swift b/apple/OmnivoreKit/Sources/App/Views/SelfHostSettingsView.swift new file mode 100644 index 000000000..4cb62cbd7 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/SelfHostSettingsView.swift @@ -0,0 +1,86 @@ +import Models +import Services +import SwiftUI +import Utils +import Views + +class SelfHostSettingsViewModel: ObservableObject { + @State var showCreateError = false +} + +struct SelfHostSettingsView: View { + @State var apiServerAddress = "" + @State var webServerAddress = "" + @State var ttsServerAddress = "" + + @State var showConfirmAlert = false + + @Environment(\.dismiss) private var dismiss + + @EnvironmentObject var dataService: DataService + @StateObject var viewModel = SelfHostSettingsViewModel() + + var allFieldsSet: Bool { + apiServerAddress.count > 0 && webServerAddress.count > 0 && ttsServerAddress.count > 0 + } + + var saveButton: some View { + Button(action: { + showConfirmAlert = true + }, label: { + Text("Save") + }) + .disabled(!allFieldsSet) + } + + var body: some View { + Form { + Section("API Server Base URL") { + TextField("URL", text: $apiServerAddress, prompt: Text("https://api-prod.omnivore.app")) + .keyboardType(.URL) + } + + Section("Web Server URL") { + TextField("URL", text: $webServerAddress, prompt: Text("https://omnivore.app")) + .keyboardType(.URL) + } + + Section("Text-to-speech Server URL") { + TextField("URL", text: $ttsServerAddress, prompt: Text("https://tts.omnivore.app")) + .keyboardType(.URL) + } + + Section { + Section { + Text(""" + Omnivore is a free and open-source project and allows self-hosting. + + If you have chosen to deploy your own server instance, fill in the \ + above fields to connect to your private self-hosted instance. + + [Learn more about self-hosting Omnivore](https://docs.omnivore.app/self-hosting/self-hosting.html) + """) + .accentColor(.blue) + } + } + } + .accentColor(.appGrayText) + .alert(isPresented: $showConfirmAlert) { + Alert( + title: Text("Changing your environment settings will close the app."), + dismissButton: .cancel(Text("Ok")) { + AppEnvironment.setCustom(serverBaseURL: apiServerAddress, webAppBaseURL: webServerAddress, ttsBaseURL: ttsServerAddress) + dataService.switchAppEnvironment(appEnvironment: AppEnvironment.custom) + } + ) + } + .navigationViewStyle(.stack) + .navigationTitle("Self-hosting Options") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(leading: + Button(action: { + dismiss() + }, label: { Text("Cancel") }), + trailing: saveButton) + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift b/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift index ab9362812..1776f7544 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift @@ -18,6 +18,7 @@ struct WelcomeView: View { @State private var showTermsModal = false @State private var showPrivacyModal = false @State private var showEmailLoginModal = false + @State private var showAdvancedLogin = false @State private var showAboutPage = false @State private var selectedEnvironment = AppEnvironment.initialAppEnvironment @State private var containerSize: CGSize = .zero @@ -83,6 +84,8 @@ struct WelcomeView: View { Button("View Privacy Policy") { showPrivacyModal = true } + + Spacer() } .sheet(isPresented: $showPrivacyModal) { VStack { @@ -235,6 +238,18 @@ struct WelcomeView: View { } footerView Spacer() + + Button( + action: { showAdvancedLogin = true }, + label: { + Text("Self-hosting options") + .font(Font.appCaption) + .foregroundColor(.appGrayTextContrast) + .underline() + .frame(maxWidth: .infinity, alignment: .center) + } + ) + .padding(.vertical) } .padding() .sheet(isPresented: $showEmailLoginModal) { @@ -243,6 +258,11 @@ struct WelcomeView: View { .sheet(isPresented: $showDebugModal) { DebugMenuView(selectedEnvironment: $selectedEnvironment) } + .sheet(isPresented: $showAdvancedLogin) { + NavigationView { + SelfHostSettingsView() + } + } .alert(deletedAccountConfirmationMessage, isPresented: $authenticator.showAppleRevokeTokenAlert) { Button("View Details") { openURL(URL(string: "https://support.apple.com/en-us/HT210426")!) diff --git a/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift b/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift index 1c5cc0c6c..d3f53eb49 100644 --- a/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift +++ b/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift @@ -3,10 +3,10 @@ import Utils public enum AppEnvironment: String { case local - case dev case prod case demo case test + case custom public static let initialAppEnvironment: AppEnvironment = { #if DEBUG @@ -32,47 +32,68 @@ private let devWebURL = "https://web-dev.omnivore.app" private let demoWebURL = "https://demo.omnivore.app" private let prodWebURL = "https://omnivore.app" +private enum AppEnvironmentUserDefaultKey: String { + case serverBaseURL = "AppEnvironment_serverBaseURL" + case webAppBaseURL = "AppEnvironment_webAppBaseURL" + case ttsBaseURL = "AppEnvironment_ttsBaseURL" +} + public extension AppEnvironment { + static func setCustom(serverBaseURL: String, webAppBaseURL: String, ttsBaseURL: String) { + UserDefaults.standard.set(serverBaseURL, forKey: AppEnvironmentUserDefaultKey.serverBaseURL.rawValue) + UserDefaults.standard.set(webAppBaseURL, forKey: AppEnvironmentUserDefaultKey.webAppBaseURL.rawValue) + UserDefaults.standard.set(ttsBaseURL, forKey: AppEnvironmentUserDefaultKey.ttsBaseURL.rawValue) + } + var graphqlPath: String { "\(serverBaseURL.absoluteString)/api/graphql" } var serverBaseURL: URL { switch self { - case .dev: - return URL(string: devBaseURL)! case .demo: return URL(string: demoBaseURL)! case .prod: return URL(string: prodBaseURL)! case .test, .local: return URL(string: "http://localhost:4000")! + case .custom: + guard let str = UserDefaults.standard.string(forKey: AppEnvironmentUserDefaultKey.serverBaseURL.rawValue), let url = URL(string: str) else { + fatalError("custom serverBaseURL not set") + } + return url } } var webAppBaseURL: URL { switch self { - case .dev: - return URL(string: devWebURL)! case .demo: return URL(string: demoWebURL)! case .prod: return URL(string: prodWebURL)! case .test, .local: return URL(string: "http://localhost:3000")! + case .custom: + guard let str = UserDefaults.standard.string(forKey: AppEnvironmentUserDefaultKey.webAppBaseURL.rawValue), let url = URL(string: str) else { + fatalError("custom webAppBaseURL not set") + } + return url } } var ttsBaseURL: URL { switch self { - case .dev: - return URL(string: "notimplemented")! case .demo: return URL(string: demoTtsURL)! case .prod: return URL(string: prodTtsURL)! case .test, .local: return URL(string: "http://localhost:4000")! + case .custom: + guard let str = UserDefaults.standard.string(forKey: AppEnvironmentUserDefaultKey.ttsBaseURL.rawValue), let url = URL(string: str) else { + fatalError("custom ttsBaseURL not set") + } + return url } } }