Skip to content
This repository has been archived by the owner on Aug 11, 2024. It is now read-only.

Rework Invite Code Experience #735

Merged
merged 29 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
31828c4
Remodel invite code flow
bfollington Jun 20, 2023
41923e6
Create sphere upfront, request gateway ID on blur
bfollington Jun 20, 2023
4889584
Playtesting UX rough edges
bfollington Jun 20, 2023
faccb99
Formatting
bfollington Jun 20, 2023
8ff988d
Update packges
bfollington Jun 20, 2023
4428dae
Update packages
bfollington Jun 28, 2023
4bc8c03
Re-arrange settings view
bfollington Jun 28, 2023
f7cc299
Formatting
bfollington Jun 28, 2023
84b70fd
Formatting and copy
bfollington Jun 28, 2023
24e49b5
Remodel navigation for first run
bfollington Jun 28, 2023
e2a487a
Introduce API to mark field as invalid/valid
bfollington Jun 28, 2023
3d67c86
Remove extra error section
bfollington Jun 28, 2023
8364efc
Improve invite code lifecycle
bfollington Jun 28, 2023
deabbde
Add explicit Request Offline action
bfollington Jun 28, 2023
7a1cc2d
Add tests for FirstRun validation
bfollington Jun 28, 2023
c5cf5e4
Drop unused action
bfollington Jun 28, 2023
a00ef9d
Consolidate duplicated actions
bfollington Jun 28, 2023
572d0cb
Add comment
bfollington Jun 28, 2023
ede74de
Increase detail of assertions
bfollington Jun 28, 2023
bc8796c
Add tests for validation status override
bfollington Jun 28, 2023
8940207
Fix QR code scanning -> populate field
bfollington Jun 28, 2023
1d2b20a
Add diagrams to docs folder
bfollington Jun 28, 2023
6056833
Break settings sections out into separate files
bfollington Jun 29, 2023
059b2f1
Formatting
bfollington Jun 29, 2023
914e4c3
Surface UI error for missing sphere
bfollington Jun 29, 2023
3d975c6
Tweak copy
bfollington Jun 29, 2023
e935814
Remove .initial case from FirstRunStep
bfollington Jun 29, 2023
e23078d
Rename .nickname -> .profile
bfollington Jun 29, 2023
99ff1be
Rename .connect -> .done
bfollington Jun 29, 2023
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
Binary file added docs/gateway-provisioning/first-run.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/gateway-provisioning/settings-view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
335 changes: 254 additions & 81 deletions xcode/Subconscious/Shared/Components/AppView.swift

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ struct ValidatedFormField<T: Equatable>: View {
var autoFocus: Bool = false
var submitLabel: SubmitLabel = .done
var onSubmit: () -> Void = {}
var onFocusChanged: (_ focused: Bool) -> Void = { _ in }

var backgroundColor = Color.background

Expand All @@ -32,10 +33,6 @@ struct ValidatedFormField<T: Equatable>: View {
return this
}

var isValid: Bool {
!field.shouldPresentAsInvalid
}

var body: some View {
VStack(alignment: alignment, spacing: AppTheme.unit2) {
HStack {
Expand All @@ -54,16 +51,22 @@ struct ValidatedFormField<T: Equatable>: View {
.background(backgroundColor)
}
.padding(.trailing, 1)
.opacity(isValid ? 0 : 1)
.animation(.default, value: isValid)
.opacity(field.shouldPresentAsInvalid ? 1 : 0)
.animation(.default, value: field.shouldPresentAsInvalid)
}
.onChange(of: focused) { focused in
send(.focusChange(focused: focused))
onFocusChanged(focused)
}
.onChange(of: innerText, perform: { innerText in
.onChange(of: innerText) { innerText in
send(.setValue(input: innerText))
})
.submitLabel(submitLabel)
}
.onChange(of: field) { field in
// The has been reset, sync inner value
if !field.touched && innerText != field.value {
innerText = field.value
}
bfollington marked this conversation as resolved.
Show resolved Hide resolved
}
.onSubmit {
onSubmit()
}
Expand All @@ -75,9 +78,9 @@ struct ValidatedFormField<T: Equatable>: View {
}
Text(caption)
.foregroundColor(
isValid ? Color.secondary : Color.red
field.shouldPresentAsInvalid ? Color.red : Color.secondary
)
.animation(.default, value: isValid)
.animation(.default, value: field.shouldPresentAsInvalid)
.font(.caption)
}
.onAppear {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ struct FollowNewUserFormSheetModel: ModelProtocol {
return update(
state: state,
actions: [
.form(.didField(.markAsTouched)),
.form(.didField(.reset)),
.form(.didField(.setValue(input: content)))
],
environment: environment
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// ResourceSyncBadge.swift
// Subconscious (iOS)
//
// Created by Ben Follington on 30/6/2023.
//

import Foundation
import SwiftUI

struct PendingSyncBadge: View {
@State var spin = false

var body: some View {
Image(systemName: "arrow.triangle.2.circlepath")
.rotationEffect(.degrees(spin ? 360 : 0))
.animation(Animation.linear
.repeatForever(autoreverses: false)
.speed(0.4), value: spin)
.task{
self.spin = true
}
}
}

struct ResourceSyncBadge: View {
var status: ResourceStatus

var body: some View {
switch status {
case .initial:
Image(systemName: "arrow.triangle.2.circlepath")
.foregroundColor(.secondary)
case .pending:
PendingSyncBadge()
.foregroundColor(.secondary)
case .succeeded:
Image(systemName: "checkmark.circle")
.foregroundColor(.secondary)
case .failed:
Image(systemName: "exclamationmark.arrow.triangle.2.circlepath")
.foregroundColor(.red)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ struct FirstRunDoneView: View {
@Environment(\.colorScheme) var colorScheme

private var status: ResourceStatus {
app.state.gatewayProvisioningStatus
if app.state.lastGatewaySyncStatus == .succeeded {
return app.state.lastGatewaySyncStatus
}
bfollington marked this conversation as resolved.
Show resolved Hide resolved

return app.state.gatewayProvisioningStatus
}

private var dottedLine: some View {
Expand Down Expand Up @@ -88,7 +92,7 @@ struct FirstRunDoneView: View {
Spacer()
Button(
action: {
app.send(.persistFirstRunComplete(true))
app.send(.submitFirstRunDoneStep)
}
) {
Text("Begin")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ struct FirstRunProfileView: View {
autoFocus: true,
submitLabel: .go,
onSubmit: {
if app.state.nicknameFormField.isValid {
app.send(.pushFirstRunStep(.sphere))
}
app.send(.submitFirstRunProfileStep)
}
)
.textFieldStyle(.roundedBorder)
Expand All @@ -48,12 +46,11 @@ struct FirstRunProfileView: View {
Spacer()

if !app.state.nicknameFormField.hasFocus {
NavigationLink(
value: FirstRunStep.sphere,
label: {
Text("Continue")
}
)
Button(action: {
app.send(.submitFirstRunProfileStep)
}, label: {
Text("Continue")
})
.buttonStyle(PillButtonStyle())
.disabled(!app.state.nicknameFormField.isValid)
}
Expand All @@ -65,16 +62,6 @@ struct FirstRunProfileView: View {
AppTheme.onboarding
.appBackgroundGradient(colorScheme)
)
.onAppear {
app.send(.createSphere)
}
bfollington marked this conversation as resolved.
Show resolved Hide resolved
.onDisappear {
guard let nickname = app.state.nicknameFormField.validated else {
return
}

app.send(.submitNickname(nickname))
}
bfollington marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ struct FirstRunRecoveryView: View {
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
Spacer()
NavigationLink(
value: FirstRunStep.connect,
label: {
Text("Ok, I wrote it down")
}
)

Button(action: {
app.send(.submitFirstRunRecoveryStep)
}, label: {
Text("Ok, I wrote it down")
})
.buttonStyle(PillButtonStyle())
}
.padding()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,11 @@ struct FirstRunSphereView: View {

Spacer()

NavigationLink(
value: FirstRunStep.recovery,
label: {
Text("Got it")
}
)
Button(action: {
app.send(.submitFirstRunSphereStep)
}, label: {
Text("Got it")
})
.buttonStyle(PillButtonStyle())
}
.padding()
Expand Down
123 changes: 83 additions & 40 deletions xcode/Subconscious/Shared/Components/FirstRun/FirstRunView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@
import ObservableStore
import SwiftUI

struct InviteCodeRedeemedView: View {
var gatewayId: String

var body: some View {
VStack(spacing: AppTheme.unit2) {
HStack(spacing: AppTheme.unit2) {
Image(systemName: "checkmark.circle")
.resizable()
.frame(width: 16, height: 16)
.opacity(0.5)
Text("Invitation accepted")
}
.font(.body)
.bold()

HStack(spacing: AppTheme.unit) {
Text("Gateway ID")
.bold()
Text(gatewayId)
}
.font(.caption)
}
.foregroundColor(.secondary)
}
}

struct FirstRunView: View {
@ObservedObject var app: Store<AppModel>
@Environment(\.colorScheme) var colorScheme
Expand Down Expand Up @@ -46,55 +72,69 @@ struct FirstRunView: View {

Spacer()

ValidatedFormField(
alignment: .center,
placeholder: "Enter your invite code",
field: app.state.inviteCodeFormField,
send: Address.forward(
send: app.send,
tag: AppAction.inviteCodeFormField
),
caption: "Look for this in your welcome email.",
submitLabel: .go,
onSubmit: {
if app.state.inviteCodeFormField.isValid {
app.send(.pushFirstRunStep(.nickname))
if let gatewayId = app.state.gatewayId {
InviteCodeRedeemedView(
gatewayId: gatewayId
)
} else {
ValidatedFormField(
alignment: .center,
placeholder: "Enter your invite code",
field: app.state.inviteCodeFormField,
send: Address.forward(
send: app.send,
tag: AppAction.inviteCodeFormField
),
caption: Func.run {
switch app.state.inviteCodeRedemptionStatus {
case .failed(_):
return "Could not redeem invite code"
case _:
return "You can find your invite code in your welcome email"
}
},
onFocusChanged: { focused in
// User finished editing the field
if !focused {
app.send(.submitInviteCodeForm)
}
}
}
)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)

)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.disabled(app.state.gatewayOperationInProgress)
}

if !app.state.inviteCodeFormField.hasFocus {
Spacer()

NavigationLink(
value: FirstRunStep.nickname,
label: {
Text("Get Started")
}
)
Button(action: {
app.send(.submitFirstRunWelcomeStep)
}, label: {
Text("Get Started")
})
.buttonStyle(PillButtonStyle())
.disabled(!app.state.inviteCodeFormField.isValid)
.disabled(app.state.gatewayId == nil)
bfollington marked this conversation as resolved.
Show resolved Hide resolved
}

// MARK: Use Offline
HStack(spacing: AppTheme.unit) {
Text("No invite code?")
.font(.caption)
.foregroundColor(.secondary)

NavigationLink(
value: FirstRunStep.nickname,
label: {
Text("Use offline")
VStack {
if app.state.gatewayId == .none {
HStack(spacing: AppTheme.unit) {
Text("No invite code?")
.font(.caption)
.foregroundColor(.secondary)

Button(action: {
app.send(.requestOfflineMode)
}, label: {
Text("Use offline")
.font(.caption)
})
}
)
}
.padding(
}
}.padding(
.init(
top: 0,
leading: 0,
Expand All @@ -114,16 +154,19 @@ struct FirstRunView: View {
for: FirstRunStep.self
) { step in
switch step {
case .nickname:
case .profile:
FirstRunProfileView(app: app)
case .sphere:
FirstRunSphereView(app: app)
case .recovery:
FirstRunRecoveryView(app: app)
case .connect:
case .done:
FirstRunDoneView(app: app)
}
}
.onAppear {
app.send(.createSphere)
}
bfollington marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down
Loading