@@ -185,121 +185,140 @@ public struct MFAEnrolmentView {
185
185
showCopiedFeedback = false
186
186
}
187
187
}
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
+ }
188
209
}
189
210
190
211
extension MFAEnrolmentView : View {
191
212
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 ( )
202
218
}
203
- . padding ( . horizontal)
219
+ . foregroundColor ( . blue)
220
+ . accessibilityIdentifier ( " cancel-button " )
221
+ Spacer ( )
222
+ }
223
+ . padding ( . horizontal)
204
224
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 ( )
211
238
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)
214
255
. foregroundColor ( . secondary)
215
256
. 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)
226
265
227
- Text ( " Multi-Factor Authentication Disabled " )
228
- . font ( . title2)
229
- . fontWeight ( . semibold)
266
+ Text ( " No Authentication Methods Available " )
267
+ . font ( . title2)
268
+ . fontWeight ( . semibold)
230
269
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. " )
234
271
. font ( . body)
235
272
. foregroundColor ( . secondary)
236
273
. 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)
270
289
}
271
290
}
272
- . pickerStyle ( . segmented)
273
- . accessibilityIdentifier ( " factor-type-picker " )
274
291
}
275
- . padding ( . horizontal)
292
+ . pickerStyle ( . segmented)
293
+ . accessibilityIdentifier ( " factor-type-picker " )
276
294
}
295
+ . padding ( . horizontal)
277
296
}
297
+ }
278
298
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
+ }
285
305
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 " )
294
313
}
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
303
322
}
304
323
}
305
324
}
@@ -493,26 +512,28 @@ extension MFAEnrolmentView: View {
493
512
. foregroundColor ( . secondary)
494
513
. multilineTextAlignment ( . center)
495
514
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
+ )
516
537
}
517
538
518
539
Text ( " Manual Entry Key: " )
0 commit comments