Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,4 @@ Alfie/AlfieKit/apollo-ios-cli
!*.secret
Alfie/Alfie/Configuration/Debug/GoogleService-Info.plist
Alfie/Alfie/Configuration/Release/GoogleService-Info.plist
Alfie/Alfie Companion/GoogleService-Info.plist
10 changes: 10 additions & 0 deletions Alfie/Alfie Companion/Alfie Companion.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.mindera.alfie.shared</string>
</array>
</dict>
</plist>
15 changes: 15 additions & 0 deletions Alfie/Alfie Companion/Alfie_CompanionApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Firebase
import SwiftUI

@main
struct Alfie_CompanionApp: App {
init() {
FirebaseApp.configure()
}

var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"images" : [
"colors" : [
{
"filename" : "4.png",
"idiom" : "universal"
}
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
79 changes: 79 additions & 0 deletions Alfie/Alfie Companion/CompanionStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import Foundation
import Model
import Combine

private let appGroupSuiteName = "group.com.mindera.alfie.shared"
private let endpointKey = "com.alfie.config.api.endpoint"

final class CompanionStore: ObservableObject {
@Published private(set) var flagOverrides: [String: Bool] = [:]
@Published private(set) var endpointUrlString: String?
@Published private(set) var allGroupEntries: [(key: String, value: String)] = []

private let suite: UserDefaults

init() {
self.suite = UserDefaults(suiteName: appGroupSuiteName) ?? .standard
reload()
}

// MARK: - Reload

func reload() {
var overrides: [String: Bool] = [:]
for key in ConfigurationKey.allCases {
if let val = suite.object(forKey: key.rawValue) as? Bool {
overrides[key.rawValue] = val
}
}
flagOverrides = overrides
endpointUrlString = suite.string(forKey: endpointKey)

let raw = suite.dictionaryRepresentation()
allGroupEntries = raw
.filter { !$0.key.hasPrefix("Apple") }
.map { (key: $0.key, value: "\($0.value)") }
.sorted { $0.key < $1.key }
}

// MARK: - Flags

func setFlag(rawKey: String, value: Bool) {
suite.set(value, forKey: rawKey)
reload()
}

func removeFlag(rawKey: String) {
suite.removeObject(forKey: rawKey)
reload()
}

// MARK: - Endpoint

func setEndpoint(urlString: String) {
suite.set(urlString, forKey: endpointKey)
reload()
}

func removeEndpoint() {
suite.removeObject(forKey: endpointKey)
reload()
}

// MARK: - App Group browser

func removeEntry(key: String) {
suite.removeObject(forKey: key)
reload()
}

// MARK: - Reset All

func resetAll() {
for key in ConfigurationKey.allCases {
suite.removeObject(forKey: key.rawValue)
}
suite.removeObject(forKey: endpointKey)
reload()
}
}
28 changes: 28 additions & 0 deletions Alfie/Alfie Companion/ContentView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import SwiftUI

struct ContentView: View {
@StateObject private var store = CompanionStore()

var body: some View {
TabView {
NavigationStack {
FlagsView(store: store)
}
.tabItem { Label("Flags", systemImage: "flag") }

NavigationStack {
EndpointView(store: store)
}
.tabItem { Label("Endpoint", systemImage: "network") }

NavigationStack {
InspectorView(store: store)
}
.tabItem { Label("Inspector", systemImage: "magnifyingglass") }
}
}
}

#Preview {
ContentView()
}
106 changes: 106 additions & 0 deletions Alfie/Alfie Companion/EndpointView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import Model
import SwiftUI

struct EndpointView: View {
@ObservedObject var store: CompanionStore
@State private var selectedOption: ApiEndpointOption = .dev
@State private var customUrlText = ""

var body: some View {
List {
Section {
currentOverrideSummary
} header: {
Text("Current Override")
}

Section {
Picker("Endpoint", selection: $selectedOption) {
ForEach(namedOptions, id: \.self) { option in
Text(option.label).tag(option)
}
Text("Custom").tag(ApiEndpointOption.custom(url: nil))
}
.pickerStyle(.inline)
.labelsHidden()

if case .custom = selectedOption {
TextField("https://…", text: $customUrlText)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.keyboardType(.URL)
}
} header: {
Text("Select Override")
}

Section {
Button("Apply Override") { applyOverride() }
.disabled(!canApply)
Button("Remove Override", role: .destructive) {
store.removeEndpoint()
syncFromStore()
}
.disabled(store.endpointUrlString == nil)
} footer: {
Text("Restart the main app to apply changes.")
}
}
.navigationTitle("Endpoint")
.onAppear { syncFromStore() }
}

private var namedOptions: [ApiEndpointOption] {
[.dev, .preProd, .prod]
}

private var currentOverrideSummary: some View {
Group {
if let url = store.endpointUrlString {
Text(url)
.font(.system(.body, design: .monospaced))
} else {
Text("None — default applies")
.foregroundStyle(.secondary)
}
}
}

private var canApply: Bool {
if case .custom = selectedOption {
return !customUrlText.isEmpty && URL(string: customUrlText) != nil
}
return true
}

private func applyOverride() {
let urlString: String
switch selectedOption {
case .dev:
urlString = "http://localhost:3000/"
case .preProd:
urlString = "https://preprod.bff.tbd.invalid/"
case .prod:
urlString = "https://prod.bff.tbd.invalid/"
case .custom:
urlString = customUrlText
}
store.setEndpoint(urlString: urlString)
}

private func syncFromStore() {
guard let stored = store.endpointUrlString else {
selectedOption = .dev
customUrlText = ""
return
}
let known: [ApiEndpointOption] = [.dev, .preProd, .prod]
let knownUrls = ["http://localhost:3000/", "https://preprod.bff.tbd.invalid/", "https://prod.bff.tbd.invalid/"]
if let idx = knownUrls.firstIndex(of: stored) {
selectedOption = known[idx]
} else {
selectedOption = .custom(url: URL(string: stored))
customUrlText = stored
}
}
}
Loading
Loading