~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

The iOS app's sign-in screen is the tilde over a handle and a button

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:ios/sign-in
ios/Knotless/API/Account.swift+74−28
@@ -87,13 +87,12 @@ final class Account {
}
}
/// Logs in through the browser, as `handle`, or making an account.
func signIn(handle: String = "", create: Bool = false) async {
/// Logs in through the browser, as `handle`.
func signIn(handle: String) async {
failure = nil
let pkce = PKCE()
var parts = URLComponents(url: Client.site.appendingPathComponent("oauth/app-login"), resolvingAgainstBaseURL: false)!
parts.queryItems = [URLQueryItem(name: "challenge", value: pkce.challenge)]
+ (create ? [URLQueryItem(name: "create", value: "true")] : [URLQueryItem(name: "handle", value: handle)])
parts.queryItems = [URLQueryItem(name: "challenge", value: pkce.challenge), URLQueryItem(name: "handle", value: handle)]
let opened = Date()
do {
let back = try await browse(parts.url!)
@@ -150,44 +149,91 @@ private final class BrowserAnchor: NSObject, ASWebAuthenticationPresentationCont
}
}
/// Asks for a handle and goes to the browser.
/// The tilde over a handle to sign in with, which goes to the browser.
struct SignInForm: View {
@Environment(Account.self) private var account
@State private var handle = ""
@State private var busy = false
@FocusState private var typing: Bool
var why: String
private var name: String { handle.trimmingCharacters(in: .whitespaces) }
private var ready: Bool { !name.isEmpty && !busy }
var body: some View {
Form {
Section {
TextField("alice.bsky.social", text: $handle)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.URL)
.submitLabel(.go)
.onSubmit { go(create: false) }
Button("Sign in") { go(create: false) }
.disabled(handle.trimmingCharacters(in: .whitespaces).isEmpty || busy)
} header: {
Text("Your handle")
} footer: {
Text(why)
}
Section {
Button("Make an account") { go(create: true) }.disabled(busy)
} footer: {
Text("An Atmosphere account is one you can use across apps.")
VStack(spacing: Space.s3) {
Spacer(minLength: Space.s4)
Image("mark").renderingMode(.template).resizable().scaledToFit()
.frame(width: 112)
.foregroundStyle(.primary)
.accessibilityLabel("knotless")
Spacer(minLength: Space.s4)
TextField("alice.bsky.social", text: $handle)
.textContentType(.username)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.URL)
.submitLabel(.go)
.focused($typing)
.onSubmit(go)
.padding(.horizontal, Space.s5)
.frame(minHeight: Size.signIn)
.background(Color(.secondarySystemBackground), in: Capsule())
.accessibilityLabel("Your handle")
Button(action: go) {
Group {
if busy {
ProgressView().tint(Color(.systemBackground))
} else {
Text("Sign in").font(.body.weight(.semibold))
}
}
.foregroundStyle(ready || busy ? Color(.systemBackground) : Color.secondary)
.frame(maxWidth: .infinity, minHeight: Size.signIn)
.background(ready || busy ? Color.primary : Color(.tertiarySystemFill), in: Capsule())
.contentShape(Capsule())
}
if let failure = account.failure {
Section { Text(failure).foregroundStyle(Color.danger) }
.buttonStyle(.plain)
.disabled(!ready)
Group {
if let failure = account.failure {
Text(failure).foregroundStyle(Color.danger)
} else {
Text(why).foregroundStyle(.secondary)
}
}
.font(.footnote)
.multilineTextAlignment(.center)
Text(Self.terms)
.font(.footnote)
.multilineTextAlignment(.center)
.tint(Color.accent)
.padding(.top, Space.s5)
.padding(.bottom, Space.s4)
}
.padding(.horizontal, Space.s6)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.onTapGesture { typing = false }
}
private func go(create: Bool) {
private static let terms: AttributedString = {
let terms = Client.site.appendingPathComponent("terms").absoluteString
let privacy = Client.site.appendingPathComponent("privacy").absoluteString
let line = "By signing in you accept our [Terms of use](\(terms)) and [Privacy policy](\(privacy))."
return (try? AttributedString(markdown: line)) ?? AttributedString(line)
}()
private func go() {
guard ready else { return }
typing = false
busy = true
Task {
await account.signIn(handle: handle.trimmingCharacters(in: .whitespaces), create: create)
await account.signIn(handle: name)
busy = false
}
}
ios/Knotless/Assets.xcassets/mark.imageset/Contents.jsonnew+1−0
@@ -0,0 +1 @@
{"images":[{"filename":"mark.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
\ No newline at end of file
ios/Knotless/Assets.xcassets/mark.imageset/mark.svgnew+1−0
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="150 385 724 254"><path d="M204.8 542.7 L211.2 536.4 L217.6 530.1 L224.0 523.8 L230.4 517.6 L236.8 511.5 L243.2 505.6 L249.6 499.7 L256.0 494.1 L262.4 488.6 L268.8 483.3 L275.2 478.3 L281.6 473.5 L288.0 469.0 L294.4 464.7 L300.8 460.7 L307.2 457.1 L313.6 453.8 L320.0 450.8 L326.4 448.1 L332.8 445.8 L339.2 443.9 L345.6 442.3 L352.0 441.1 L358.4 440.3 L364.8 439.9 L371.2 439.8 L377.6 440.1 L384.0 440.7 L390.4 441.7 L396.8 443.1 L403.2 444.8 L409.6 446.9 L416.0 449.2 L422.4 451.9 L428.8 454.9 L435.2 458.1 L441.6 461.7 L448.0 465.4 L454.4 469.4 L460.8 473.6 L467.2 478.0 L473.6 482.5 L480.0 487.2 L486.4 492.0 L492.8 496.9 L499.2 501.9 L505.6 506.9 L512.0 512.0 L518.4 517.1 L524.8 522.1 L531.2 527.1 L537.6 532.0 L544.0 536.8 L550.4 541.5 L556.8 546.0 L563.2 550.4 L569.6 554.6 L576.0 558.6 L582.4 562.3 L588.8 565.9 L595.2 569.1 L601.6 572.1 L608.0 574.8 L614.4 577.1 L620.8 579.2 L627.2 580.9 L633.6 582.3 L640.0 583.3 L646.4 583.9 L652.8 584.2 L659.2 584.1 L665.6 583.7 L672.0 582.9 L678.4 581.7 L684.8 580.1 L691.2 578.2 L697.6 575.9 L704.0 573.2 L710.4 570.2 L716.8 566.9 L723.2 563.3 L729.6 559.3 L736.0 555.0 L742.4 550.5 L748.8 545.7 L755.2 540.7 L761.6 535.4 L768.0 529.9 L774.4 524.3 L780.8 518.4 L787.2 512.5 L793.6 506.4 L800.0 500.2 L806.4 493.9 L812.8 487.6 L819.2 481.3" fill="none" stroke="#000" stroke-width="76.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
ios/Knotless/Support/Theme.swift+2−0
@@ -73,6 +73,8 @@ enum Size {
/// The least height of a row that opens a screen.
static let row: CGFloat = 56
static let fileHeader: CGFloat = 52
/// The sign-in screen's field and button.
static let signIn: CGFloat = 52
/// Room under a page for the button floating over its end.
static let floatingClearance: CGFloat = 60
/// Where a row's text starts, past its padding and a regular avatar or
ios/Knotless/Views/HomeView.swift+1−1
@@ -30,7 +30,7 @@ struct HomeView: View {
} else if account.checking {
ProgressView()
} else {
SignInForm(why: "Sign in to see your work.").navigationTitle("Home")
SignInForm(why: "Sign in to see your work.")
}
}
}
ios/Knotless/Views/InboxView.swift+6−2
@@ -17,16 +17,20 @@ struct InboxView: View {
var body: some View {
VStack(spacing: 0) {
if case .signedOut = phase {} else {
if !signedOut {
chips
Divider()
}
content.frame(maxHeight: .infinity)
}
.navigationTitle("Inbox")
.navigationTitle(signedOut ? "" : "Inbox")
.task(id: ReloadKey(filter: filter, did: account.person?.did)) { await reload() }
}
private var signedOut: Bool {
if case .signedOut = phase { true } else { false }
}
private var chips: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Space.s2) {
ios/Knotless/Views/ProfileView.swift+1−1
@@ -19,7 +19,7 @@ struct OwnProfileView: View {
} else if account.checking {
ProgressView()
} else {
SignInForm(why: "Sign in to see your profile.").navigationTitle("Profile")
SignInForm(why: "Sign in to see your profile.")
}
}
}