71 lines
2.3 KiB
Swift
71 lines
2.3 KiB
Swift
import SwiftUI
|
||
|
||
struct OtpPreviewView: View {
|
||
let seed: String
|
||
|
||
@State private var code = ""
|
||
@State private var progress = 0.0 // 0.0 – 1.0 within the current 30s window
|
||
@State private var timer: Timer?
|
||
|
||
var body: some View {
|
||
HStack(spacing: 12) {
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text("Current OTP")
|
||
.font(.caption2)
|
||
.foregroundStyle(.secondary)
|
||
Text(formatted(code))
|
||
.font(.system(size: 22, weight: .semibold, design: .monospaced))
|
||
.foregroundStyle(.primary)
|
||
}
|
||
|
||
Spacer()
|
||
|
||
ZStack {
|
||
Circle()
|
||
.stroke(Color(.systemFill), lineWidth: 3)
|
||
Circle()
|
||
.trim(from: 0, to: progress)
|
||
.stroke(progressColor, style: StrokeStyle(lineWidth: 3, lineCap: .round))
|
||
.rotationEffect(.degrees(-90))
|
||
.animation(.linear(duration: 0.8), value: progress)
|
||
Text("\(Int(progress * 30))")
|
||
.font(.caption2.monospacedDigit())
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
.frame(width: 36, height: 36)
|
||
}
|
||
.padding(.horizontal, 16)
|
||
.padding(.vertical, 10)
|
||
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))
|
||
.contentShape(Rectangle())
|
||
.onTapGesture { UIPasteboard.general.string = code }
|
||
.onAppear { refresh(); startTimer() }
|
||
.onDisappear { timer?.invalidate(); timer = nil }
|
||
}
|
||
|
||
private var progressColor: Color {
|
||
progress < 0.33 ? .red : progress < 0.6 ? .orange : .green
|
||
}
|
||
|
||
private func formatted(_ otp: String) -> String {
|
||
guard otp.count == 6 else { return otp }
|
||
return "\(otp.prefix(3)) \(otp.suffix(3))"
|
||
}
|
||
|
||
private func refresh() {
|
||
code = (try? Totp.generate(seed)) ?? Totp.generate(seed)
|
||
let seconds = Int(Date().timeIntervalSince1970) % 30
|
||
progress = Double(30 - seconds) / 30.0
|
||
}
|
||
|
||
private func startTimer() {
|
||
timer?.invalidate()
|
||
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in refresh() }
|
||
}
|
||
}
|
||
|
||
#Preview {
|
||
OtpPreviewView(seed: "JBSWY3DPEHPK3PXP")
|
||
.padding()
|
||
}
|