Skip to content

Commit 5a07afd

Browse files
authored
feat: 🎸 [0]Add Passcode Policy support to the Authentication (#1733)
* feat: 🎸 [0]Add Passcode Policy support to the Authentication * fix: 🐛 bot suggestion
1 parent f368fe2 commit 5a07afd

5 files changed

Lines changed: 655 additions & 1 deletion

File tree

Apps/Examples/Examples/FioriSwiftUICore/Onboarding/AuthenticationScreenSample.swift

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,25 @@ struct AuthenticationExamples: View {
2323
Button("Dynamic Authentication") {
2424
self.showsDynamicAuth = true
2525
}
26-
26+
27+
NavigationLink(
28+
destination: PasscodePolicyAuthenticationExample(showsIllustratedMessage: self.showsIllustratedMessage))
29+
{
30+
Text("Passcode Policy")
31+
}
32+
33+
NavigationLink(
34+
destination: CreatePasscodeFlowExample(showsIllustratedMessage: self.showsIllustratedMessage))
35+
{
36+
Text("Create Passcode Flow")
37+
}
38+
39+
NavigationLink(
40+
destination: CustomInputPasscodePolicyExample(showsIllustratedMessage: self.showsIllustratedMessage))
41+
{
42+
Text("Passcode Policy (Custom Input)")
43+
}
44+
2745
Toggle(isOn: self.$showsIllustratedMessage) {
2846
Text("Show Illustration Message")
2947
}
@@ -219,3 +237,176 @@ struct AuthenticationExample: View {
219237
messageType: self.messageType)
220238
}
221239
}
240+
241+
// MARK: - Passcode Policy (single screen)
242+
243+
/// Demonstrates a single-screen passcode entry validated against a `FioriPasscodePolicy`.
244+
struct PasscodePolicyAuthenticationExample: View {
245+
@State private var passcode: String = ""
246+
@State var showsIllustratedMessage: Bool
247+
248+
private let policy: FioriPasscodePolicy = {
249+
var policy = FioriPasscodePolicy(minLength: 8, hasDigit: true, hasUpper: true, hasSpecial: true, minUniqueChars: 4)
250+
policy.addPasscodeRule(FioriPasscodeRule(displayName: "At least 2 lowercase letters", isDisplayed: true) { passcode in
251+
passcode.unicodeScalars.filter { CharacterSet.lowercaseLetters.contains($0) }.count >= 2
252+
})
253+
return policy
254+
}()
255+
256+
var body: some View {
257+
Authentication(detailImage: {
258+
if self.showsIllustratedMessage {
259+
Image(.illustration).resizable().aspectRatio(contentMode: .fit)
260+
}
261+
}, title: {
262+
Text("Create Passcode")
263+
}, subtitle: {
264+
Text("Your passcode must meet the requirements below.")
265+
}, isDisabled: !self.policy.validate(passcode: self.passcode)) {
266+
print("passcode accepted ......")
267+
}
268+
.authenticationStyle(PasscodePolicyAuthenticationStyle(passcode: self.$passcode, policy: self.policy))
269+
.navigationBarTitle("Passcode Policy", displayMode: .inline)
270+
}
271+
}
272+
273+
// MARK: - Create Passcode Flow (two steps: create -> confirm)
274+
275+
/// Demonstrates a two-step "create passcode" flow using `PasscodePolicyAuthenticationStyle`:
276+
/// 1. `CreatePasscodeView` — enter a new passcode that must satisfy the policy.
277+
/// 2. `ConfirmPasscodeView` — re-enter the passcode; it must match the one from step 1.
278+
struct CreatePasscodeFlowExample: View {
279+
@State var showsIllustratedMessage: Bool
280+
281+
/// The policy for the "create" step.
282+
private let policy: FioriPasscodePolicy = {
283+
var policy = FioriPasscodePolicy(minLength: 8, hasDigit: true, hasUpper: true, hasSpecial: true, minUniqueChars: 4)
284+
policy.addPasscodeRule(FioriPasscodeRule(displayName: "At least 2 lowercase letters", isDisplayed: true) { passcode in
285+
passcode.unicodeScalars.filter { CharacterSet.lowercaseLetters.contains($0) }.count >= 2
286+
})
287+
return policy
288+
}()
289+
290+
var body: some View {
291+
CreatePasscodeView(policy: self.policy, showsIllustratedMessage: self.showsIllustratedMessage)
292+
}
293+
}
294+
295+
/// Step 1: enter a new passcode that satisfies the policy, then continue to confirmation.
296+
struct CreatePasscodeView: View {
297+
let policy: FioriPasscodePolicy
298+
var showsIllustratedMessage: Bool
299+
300+
@State private var passcode: String = ""
301+
@State private var showsConfirm: Bool = false
302+
303+
var body: some View {
304+
Authentication(detailImage: {
305+
if self.showsIllustratedMessage {
306+
Image(.illustration).resizable().aspectRatio(contentMode: .fit)
307+
}
308+
}, title: {
309+
Text("Create Passcode")
310+
}, subtitle: {
311+
Text("Your passcode must meet the requirements below.")
312+
}, signInAction: {
313+
FioriButton { _ in Text("Next") }
314+
}, isDisabled: !self.policy.validate(passcode: self.passcode)) {
315+
self.showsConfirm = true
316+
}
317+
.authenticationStyle(PasscodePolicyAuthenticationStyle(passcode: self.$passcode, policy: self.policy))
318+
.navigationBarTitle("Create Passcode", displayMode: .inline)
319+
.navigationDestination(isPresented: self.$showsConfirm) {
320+
ConfirmPasscodeView(originalPasscode: self.passcode, showsIllustratedMessage: self.showsIllustratedMessage)
321+
}
322+
}
323+
}
324+
325+
/// Step 2: re-enter the passcode; it must match the one created in step 1.
326+
struct ConfirmPasscodeView: View {
327+
let originalPasscode: String
328+
var showsIllustratedMessage: Bool
329+
330+
@Environment(\.dismiss) private var dismiss
331+
@State private var passcode: String = ""
332+
@State private var isPresentedBanner: Bool = false
333+
334+
/// A policy whose single displayed requirement is "matches the original passcode".
335+
private var confirmPolicy: FioriPasscodePolicy {
336+
var policy = FioriPasscodePolicy(minLength: 0)
337+
let original = self.originalPasscode
338+
policy.addPasscodeRule(FioriPasscodeRule(displayName: "Matches the passcode above", isDisplayed: true) { passcode in
339+
!passcode.isEmpty && passcode == original
340+
})
341+
return policy
342+
}
343+
344+
var body: some View {
345+
Authentication(detailImage: {
346+
if self.showsIllustratedMessage {
347+
Image(.illustration).resizable().aspectRatio(contentMode: .fit)
348+
}
349+
}, title: {
350+
Text("Confirm Passcode")
351+
}, subtitle: {
352+
Text("Re-enter your passcode to confirm.")
353+
}, signInAction: {
354+
FioriButton { _ in Text("Done") }
355+
}, isDisabled: !self.confirmPolicy.validate(passcode: self.passcode)) {
356+
print("passcode created ......")
357+
self.isPresentedBanner = true
358+
}
359+
.authenticationStyle(PasscodePolicyAuthenticationStyle(passcode: self.$passcode, policy: self.confirmPolicy))
360+
.navigationBarTitle("Confirm Passcode", displayMode: .inline)
361+
.bannerMessageView(isPresented: self.$isPresentedBanner,
362+
pushContentDown: .constant(false),
363+
icon: { EmptyView() },
364+
title: "Passcode created successfully",
365+
bannerTapped: nil,
366+
alignment: nil,
367+
messageType: .positive)
368+
}
369+
}
370+
371+
// MARK: - Passcode Policy with custom authInput
372+
373+
/// Demonstrates supplying a custom `authInput` (multiple fields / custom styles) while still
374+
/// letting `PasscodePolicyAuthenticationStyle` append the live requirement checklist below it.
375+
/// The custom passcode field and the style share the same `passcode` binding, so the checklist
376+
/// updates as the user types.
377+
struct CustomInputPasscodePolicyExample: View {
378+
@State private var name: String = ""
379+
@State private var passcode: String = ""
380+
@State var showsIllustratedMessage: Bool
381+
382+
private let policy = FioriPasscodePolicy(minLength: 8, hasDigit: true, hasUpper: true, hasSpecial: true, minUniqueChars: 4)
383+
384+
var body: some View {
385+
Authentication(detailImage: {
386+
if self.showsIllustratedMessage {
387+
Image(.illustration).resizable().aspectRatio(contentMode: .fit)
388+
}
389+
}, title: {
390+
Text("Create Passcode")
391+
}, subtitle: {
392+
Text("Custom fields with an appended requirement checklist.")
393+
}, authInput: {
394+
VStack(spacing: 16) {
395+
TextFieldFormView(title: "User Name:", text: self.$name, placeholder: "Enter your name")
396+
.textFieldFormViewStyle(AuthTextFieldStyle())
397+
.titleStyle { config in
398+
config.title.font(.fiori(forTextStyle: .headline, weight: .medium))
399+
}
400+
TextFieldFormView(title: "Passcode:", text: self.$passcode, isSecureEnabled: true, placeholder: "Enter your passcode")
401+
.textFieldFormViewStyle(AuthTextFieldStyle())
402+
.titleStyle { config in
403+
config.title.font(.fiori(forTextStyle: .headline, weight: .medium))
404+
}
405+
}
406+
}, isDisabled: self.name.isEmpty || !self.policy.validate(passcode: self.passcode)) {
407+
print("passcode accepted ......")
408+
}
409+
.authenticationStyle(PasscodePolicyAuthenticationStyle(passcode: self.$passcode, policy: self.policy))
410+
.navigationBarTitle("Custom Input", displayMode: .inline)
411+
}
412+
}

0 commit comments

Comments
 (0)