Skip to content

Commit cb6f24a

Browse files
fix: ensure barcode is visible and inputs come into view when tapped
1 parent 16ce089 commit cb6f24a

File tree

1 file changed

+134
-113
lines changed

1 file changed

+134
-113
lines changed

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAEnrolmentView.swift

Lines changed: 134 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -185,121 +185,140 @@ public struct MFAEnrolmentView {
185185
showCopiedFeedback = false
186186
}
187187
}
188+
189+
private func generateQRCode(from string: String) -> UIImage? {
190+
let data = Data(string.utf8)
191+
192+
guard let filter = CIFilter(name: "CIQRCodeGenerator") else { return nil }
193+
filter.setValue(data, forKey: "inputMessage")
194+
filter.setValue("H", forKey: "inputCorrectionLevel")
195+
196+
guard let ciImage = filter.outputImage else { return nil }
197+
198+
// Scale up the QR code for better quality
199+
let transform = CGAffineTransform(scaleX: 10, y: 10)
200+
let scaledImage = ciImage.transformed(by: transform)
201+
202+
let context = CIContext()
203+
guard let cgImage = context.createCGImage(scaledImage, from: scaledImage.extent) else {
204+
return nil
205+
}
206+
207+
return UIImage(cgImage: cgImage)
208+
}
188209
}
189210

190211
extension MFAEnrolmentView: View {
191212
public var body: some View {
192-
ScrollView {
193-
VStack(spacing: 16) {
194-
// Cancel button
195-
HStack {
196-
Button("Cancel") {
197-
cancelEnrollment()
198-
}
199-
.foregroundColor(.blue)
200-
.accessibilityIdentifier("cancel-button")
201-
Spacer()
213+
VStack(spacing: 16) {
214+
// Cancel button
215+
HStack {
216+
Button("Cancel") {
217+
cancelEnrollment()
202218
}
203-
.padding(.horizontal)
219+
.foregroundColor(.blue)
220+
.accessibilityIdentifier("cancel-button")
221+
Spacer()
222+
}
223+
.padding(.horizontal)
204224

205-
// Header
206-
VStack {
207-
Text("Set Up Two-Factor Authentication")
208-
.font(.largeTitle)
209-
.fontWeight(.bold)
210-
.multilineTextAlignment(.center)
225+
// Header
226+
VStack {
227+
Text("Set Up Two-Factor Authentication")
228+
.font(.largeTitle)
229+
.fontWeight(.bold)
230+
.multilineTextAlignment(.center)
231+
232+
Text("Add an extra layer of security to your account")
233+
.font(.subheadline)
234+
.foregroundColor(.secondary)
235+
.multilineTextAlignment(.center)
236+
}
237+
.padding()
211238

212-
Text("Add an extra layer of security to your account")
213-
.font(.subheadline)
239+
// Factor Type Selection (only if no session started)
240+
if currentSession == nil {
241+
if !authService.configuration.mfaEnabled {
242+
VStack(spacing: 12) {
243+
Image(systemName: "lock.slash")
244+
.font(.system(size: 40))
245+
.foregroundColor(.orange)
246+
247+
Text("Multi-Factor Authentication Disabled")
248+
.font(.title2)
249+
.fontWeight(.semibold)
250+
251+
Text(
252+
"MFA is not enabled in the current configuration. Please contact your administrator."
253+
)
254+
.font(.body)
214255
.foregroundColor(.secondary)
215256
.multilineTextAlignment(.center)
216-
}
217-
.padding()
218-
219-
// Factor Type Selection (only if no session started)
220-
if currentSession == nil {
221-
if !authService.configuration.mfaEnabled {
222-
VStack(spacing: 12) {
223-
Image(systemName: "lock.slash")
224-
.font(.system(size: 40))
225-
.foregroundColor(.orange)
257+
}
258+
.padding(.horizontal)
259+
.accessibilityIdentifier("mfa-disabled-message")
260+
} else if allowedFactorTypes.isEmpty {
261+
VStack(spacing: 12) {
262+
Image(systemName: "exclamationmark.triangle")
263+
.font(.system(size: 40))
264+
.foregroundColor(.orange)
226265

227-
Text("Multi-Factor Authentication Disabled")
228-
.font(.title2)
229-
.fontWeight(.semibold)
266+
Text("No Authentication Methods Available")
267+
.font(.title2)
268+
.fontWeight(.semibold)
230269

231-
Text(
232-
"MFA is not enabled in the current configuration. Please contact your administrator."
233-
)
270+
Text("No MFA methods are configured as allowed. Please contact your administrator.")
234271
.font(.body)
235272
.foregroundColor(.secondary)
236273
.multilineTextAlignment(.center)
237-
}
238-
.padding(.horizontal)
239-
.accessibilityIdentifier("mfa-disabled-message")
240-
} else if allowedFactorTypes.isEmpty {
241-
VStack(spacing: 12) {
242-
Image(systemName: "exclamationmark.triangle")
243-
.font(.system(size: 40))
244-
.foregroundColor(.orange)
245-
246-
Text("No Authentication Methods Available")
247-
.font(.title2)
248-
.fontWeight(.semibold)
249-
250-
Text("No MFA methods are configured as allowed. Please contact your administrator.")
251-
.font(.body)
252-
.foregroundColor(.secondary)
253-
.multilineTextAlignment(.center)
254-
}
255-
.padding(.horizontal)
256-
.accessibilityIdentifier("no-factors-message")
257-
} else {
258-
VStack(alignment: .leading, spacing: 12) {
259-
Text("Choose Authentication Method")
260-
.font(.headline)
261-
262-
Picker("Authentication Method", selection: $selectedFactorType) {
263-
ForEach(allowedFactorTypes, id: \.self) { factorType in
264-
switch factorType {
265-
case .sms:
266-
Image(systemName: "message").tag(SecondFactorType.sms)
267-
case .totp:
268-
Image(systemName: "qrcode").tag(SecondFactorType.totp)
269-
}
274+
}
275+
.padding(.horizontal)
276+
.accessibilityIdentifier("no-factors-message")
277+
} else {
278+
VStack(alignment: .leading, spacing: 12) {
279+
Text("Choose Authentication Method")
280+
.font(.headline)
281+
282+
Picker("Authentication Method", selection: $selectedFactorType) {
283+
ForEach(allowedFactorTypes, id: \.self) { factorType in
284+
switch factorType {
285+
case .sms:
286+
Image(systemName: "message").tag(SecondFactorType.sms)
287+
case .totp:
288+
Image(systemName: "qrcode").tag(SecondFactorType.totp)
270289
}
271290
}
272-
.pickerStyle(.segmented)
273-
.accessibilityIdentifier("factor-type-picker")
274291
}
275-
.padding(.horizontal)
292+
.pickerStyle(.segmented)
293+
.accessibilityIdentifier("factor-type-picker")
276294
}
295+
.padding(.horizontal)
277296
}
297+
}
278298

279-
// Content based on current state
280-
if let session = currentSession {
281-
enrollmentContent(for: session)
282-
} else {
283-
initialContent
284-
}
299+
// Content based on current state
300+
if let session = currentSession {
301+
enrollmentContent(for: session)
302+
} else {
303+
initialContent
304+
}
285305

286-
// Error message
287-
if !errorMessage.isEmpty {
288-
Text(errorMessage)
289-
.foregroundColor(.red)
290-
.font(.caption)
291-
.padding(.horizontal)
292-
.accessibilityIdentifier("error-message")
293-
}
306+
// Error message
307+
if !errorMessage.isEmpty {
308+
Text(errorMessage)
309+
.foregroundColor(.red)
310+
.font(.caption)
311+
.padding(.horizontal)
312+
.accessibilityIdentifier("error-message")
294313
}
295-
.padding(.horizontal, 16)
296-
.padding(.vertical, 20)
297-
.onAppear {
298-
// Initialize selected factor type to first allowed type
299-
if !allowedFactorTypes.contains(selectedFactorType),
300-
let firstAllowed = allowedFactorTypes.first {
301-
selectedFactorType = firstAllowed
302-
}
314+
}
315+
.padding(.horizontal, 16)
316+
.padding(.vertical, 20)
317+
.onAppear {
318+
// Initialize selected factor type to first allowed type
319+
if !allowedFactorTypes.contains(selectedFactorType),
320+
let firstAllowed = allowedFactorTypes.first {
321+
selectedFactorType = firstAllowed
303322
}
304323
}
305324
}
@@ -493,26 +512,28 @@ extension MFAEnrolmentView: View {
493512
.foregroundColor(.secondary)
494513
.multilineTextAlignment(.center)
495514

496-
// QR Code placeholder - in a real implementation, you'd generate and display the actual
497-
// QR code
498-
if let qrURL = totpInfo.qrCodeURL {
499-
AsyncImage(url: qrURL) { image in
500-
image
501-
.resizable()
502-
.aspectRatio(contentMode: .fit)
503-
} placeholder: {
504-
RoundedRectangle(cornerRadius: 8)
505-
.fill(Color.gray.opacity(0.3))
506-
.overlay(
507-
VStack {
508-
ProgressView()
509-
Text("Loading QR Code...")
510-
.font(.caption)
511-
}
512-
)
513-
}
514-
.frame(width: 200, height: 200)
515-
.accessibilityIdentifier("qr-code-image")
515+
// QR Code generated from the otpauth:// URI
516+
if let qrURL = totpInfo.qrCodeURL,
517+
let qrImage = generateQRCode(from: qrURL.absoluteString) {
518+
Image(uiImage: qrImage)
519+
.interpolation(.none)
520+
.resizable()
521+
.aspectRatio(contentMode: .fit)
522+
.frame(width: 200, height: 200)
523+
.accessibilityIdentifier("qr-code-image")
524+
} else {
525+
RoundedRectangle(cornerRadius: 8)
526+
.fill(Color.gray.opacity(0.3))
527+
.frame(width: 200, height: 200)
528+
.overlay(
529+
VStack {
530+
Image(systemName: "exclamationmark.triangle")
531+
.font(.title)
532+
.foregroundColor(.orange)
533+
Text("Unable to generate QR Code")
534+
.font(.caption)
535+
}
536+
)
516537
}
517538

518539
Text("Manual Entry Key:")

0 commit comments

Comments
 (0)