Skip to content

Commit

Permalink
Support adding OTPs by pasting URL
Browse files Browse the repository at this point in the history
  • Loading branch information
JustAman62 committed Oct 5, 2024
1 parent 82f03ae commit cba4b59
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 73 deletions.
6 changes: 3 additions & 3 deletions OVault/Assets.xcassets/AccentColor.colorset/Contents.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0x57",
"green" : "0x6E",
"red" : "0xDF"
"blue" : "93",
"green" : "116",
"red" : "209"
}
},
"idiom" : "universal"
Expand Down
4 changes: 2 additions & 2 deletions OVault/Services/Notifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ enum Message {
var description: String {
switch self {
case .inApp(title: let title, msg: let msg):
"In App Message: \(title), \(msg)"
"Message: \(title), \(msg)"
case .inAppError(error: let error):
"In App Error: \(error.localizedDescription)"
"Error: \(error.localizedDescription)"
}
}
}
Expand Down
137 changes: 99 additions & 38 deletions OVault/Views/AddOtpEntryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,68 +4,129 @@ import Models
struct AddOtpEntryView: View {
@State private var newEntry: OtpMetadata = .blank()
@State private var secret: String = ""
@State private var expanded: Bool = false
@State private var url: String = ""
@State private var advancedExpanded: Bool = false
@State private var tab: PageType = .manual

@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@Environment(\.notifier) private var notifier
@Environment(\.keychain) private var keychain

enum PageType {
case manual, byUrl
}

enum ValidationError: Error, LocalizedError {
case URLRequired
case AccountNameRequired
case IssuerRequired
case SecretRequired
}

private func save() {
notifier.execute {
if tab == .byUrl {
if url.isEmpty { throw ValidationError.URLRequired }
if let url = URL(string: url) {
let (otp, secret) = try OtpMetadata.from(url: url)
self.newEntry = otp
self.secret = secret
}
}

if newEntry.accountName.isEmpty { throw ValidationError.AccountNameRequired }
if newEntry.issuer.isEmpty { throw ValidationError.IssuerRequired }
if secret.isEmpty { throw ValidationError.SecretRequired }

try keychain.storeSecret(metadata: newEntry, secret: secret)
modelContext.insert(newEntry)
try modelContext.save()
DispatchQueue.main.async { dismiss() }
}
}

var body: some View {
var manualAddForm: some View {
Form {
Section {
OVTextField("Account Name", text: $newEntry.accountName, placeholder: "Acme Corp")
OVTextField("Account Name", text: $newEntry.accountName, placeholder: "Gold Account")
OVTextField("Issuer", text: $newEntry.issuer, placeholder: "Acme Corp")
}

OVTextField("Secret", text: $secret)

DisclosureGroup("Advanced") {
Picker("Algorithm", selection: $newEntry.algorithm) {
ForEach(HashAlgorithm.allCases) { alg in
Text(alg.rawValue).tag(alg)
Section {
DisclosureGroup("Advanced") {
Picker("Algorithm", selection: $newEntry.algorithm) {
ForEach(HashAlgorithm.allCases) { alg in
Text(alg.rawValue).tag(alg)
}
}
}

Picker("Length", selection: $newEntry.digits) {
Text("6").tag(6)
Text("7").tag(7)
Text("8").tag(8)
}

Picker("Type", selection: $newEntry.type) {
ForEach(OtpType.allCases, id: \.rawValue) { type in
Text(type.rawValue.uppercased()).tag(type)

Picker("Length", selection: $newEntry.digits) {
Text("6").tag(6)
Text("7").tag(7)
Text("8").tag(8)
}
}

switch newEntry.type {
case .totp:
Picker("Period", selection: $newEntry.period) {
Text("15 Seconds").tag(15)
Text("30 Seconds").tag(30)
Text("45 Seconds").tag(45)
Text("60 Seconds").tag(60)

Picker("Type", selection: $newEntry.type) {
ForEach(OtpType.allCases, id: \.rawValue) { type in
Text(type.rawValue.uppercased()).tag(type)
}
}
case .hotp:
LabeledContent {
TextField(value: $newEntry.counter, format: .number, label: { EmptyView() })
} label: {
Text("Counter")

switch newEntry.type {
case .totp:
Picker("Period", selection: $newEntry.period) {
Text("15 Seconds").tag(15)
Text("30 Seconds").tag(30)
Text("45 Seconds").tag(45)
Text("60 Seconds").tag(60)
}
case .hotp:
LabeledContent {
TextField(value: $newEntry.counter, format: .number, label: { EmptyView() })
} label: {
Text("Counter")
}
@unknown default:
EmptyView()
}
@unknown default:
EmptyView()
}
}
}
.formStyle(.grouped)
}

var fromUrlForm: some View {
Form {
Section {
OVTextField("URL", text: $url, placeholder: "otpauth://totp/Example:alice@example.com?secret=ABCDEFGHIJKLMNOP")
}
}
.formStyle(.grouped)
}

var body: some View {
VStack {
Picker("Type", selection: $tab) {
Text("Manual").tag(PageType.manual)
Text("From URL").tag(PageType.byUrl)
}
.pickerStyle(.segmented)
.labelsHidden()
.padding([.horizontal, .top])

switch tab {
case .byUrl:
fromUrlForm
.transition(.opacity)
case .manual:
manualAddForm
.transition(.opacity)
}

Spacer()

#if os(macOS)
HStack {
Expand All @@ -75,13 +136,13 @@ struct AddOtpEntryView: View {
Spacer()
Button("Save", action: save)
}
.padding(.vertical)
.padding()
#endif
}
.animation(.easeInOut, value: tab)
.navigationTitle("New OTP")
#if os(macOS)
.padding()
#else
#if !os(macOS)
.background(Color(uiColor: .systemGroupedBackground))
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Save", action: save)
Expand Down
41 changes: 21 additions & 20 deletions OVault/Views/EditOtpEntryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,30 @@ struct EditOtpEntryView: View {
}

var body: some View {
Form {
Section {
OVTextField("Account Name", text: $otp.accountName)
OVTextField("Issuer", text: $otp.issuer)
}

Section {
LabeledContent("Secret") {
if let secret {
Text(secret)
.textSelection(.enabled)
} else {
Button("Reveal Secret") {
notifier.execute {
self.secret = try keychain.getSecret(metadata: otp)
VStack {
Form {
Section {
OVTextField("Account Name", text: $otp.accountName)
OVTextField("Issuer", text: $otp.issuer)
}

Section {
LabeledContent("Secret") {
if let secret {
Text(secret)
.textSelection(.enabled)
} else {
Button("Reveal Secret") {
notifier.execute {
self.secret = try keychain.getSecret(metadata: otp)
}
}
}
}
}
}

.formStyle(.grouped)

#if os(macOS)
HStack {
Button("Cancel", role: .cancel) {
Expand All @@ -48,13 +51,11 @@ struct EditOtpEntryView: View {
Spacer()
Button("Save", action: save)
}
.padding(.vertical)
.padding()
#endif
}
.navigationTitle("Edit OTP")
#if os(macOS)
.padding()
#else
#if !os(macOS)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Save", action: save)
Expand Down
23 changes: 13 additions & 10 deletions OVault/Views/OtpEntryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ struct OtpEntryView: View {
@Bindable var otp: OtpMetadata

@State private var calculated: String = ""
@State private var expiresIn: Double = 0.0

@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@Environment(\.notifier) private var notifier
Expand Down Expand Up @@ -34,7 +36,8 @@ struct OtpEntryView: View {
Text(calculated)
.font(.title)
.textSelection(.enabled)

.animation(.easeInOut, value: calculated)

Spacer()
CopyButton("Copy", value: calculated)
.font(.caption)
Expand All @@ -46,17 +49,17 @@ struct OtpEntryView: View {
TimelineView(.animation) { _ in
ProgressView(value: otp.expiresIn, total: Double(otp.period))
.progressViewStyle(.linear)
.onChange(of: otp.timeStep, initial: true) {
notifier.execute {
try calculated = keychain.getOtp(metadata: otp)
.onChange(of: otp.timeStep, initial: true) {
notifier.execute {
try calculated = keychain.getOtp(metadata: otp)
}
}
}
.onChange(of: otp.counter, initial: true) {
notifier.execute {
try calculated = keychain.getOtp(metadata: otp)
.onChange(of: otp.counter) {
notifier.execute {
try calculated = keychain.getOtp(metadata: otp)
}
}
}
.animation(.linear, value: otp.expiresIn)
.animation(.linear, value: otp.expiresIn)
}
}
.contentShape(.rect)
Expand Down

0 comments on commit cba4b59

Please sign in to comment.