@@ -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
190211extension 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