ChangeCredential
The ChangeCredential view renders a row that lets a signed-in user set a new value for one of their own credentials. Tapping it opens a sheet that collects only a new value and its confirmation, checks it against the applicable rules, and posts the change via ThunderIDClient.updateUserCredentials.
It reads the rules it needs from the user type schema (GET /users/me/meta), fetched once by ThunderIDState and shared with any other mounted view that needs it (for example UserProfile). Multiple views cost one request rather than one each. The default label and messages are built from the credential attribute's own displayName in that schema, so they always match whatever an admin named it there.
By default it manages the password credential. To manage a different one, for example a PIN declared on the user type schema, set attribute; render one instance per credential to let a user manage more than one.
ChangeCredential requires .thunderIDProvider(config:) in its ancestor view hierarchy.
ChangeCredential collects only a new value and its confirmation, not the account's existing value. The self-service credential write path does not verify the current value today, so asking for one would only teach the user a false sense of security.
Usage
import SwiftUI
import ThunderIDSwiftUI
struct SecuritySection: View {
var body: some View {
VStack(alignment: .leading) {
Text("Security")
ChangeCredential()
}
}
}
Managing a Different Credential
Render one instance per credential the user type schema declares, keyed by its attribute name:
VStack(alignment: .leading) {
ChangeCredential()
ChangeCredential(attribute: "pin")
}
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
attribute | String | ❌ | The credential attribute this instance manages, any attribute the user type schema declares credential: true for. Defaults to "password". |
onSuccess | (() -> Void)? | ❌ | Called after the credential has been changed successfully. |
The display name always comes from the schema's displayName for attribute (falling back to the title-cased attribute name when the schema declares none). Rename it from the ThunderID console rather than overriding it in code: ChangeCredential has no parameter for that, so the schema stays the single source of truth.
Behavior
- Renders as a row, matching
UserProfile's field rows, that opens a compact sheet on tap. - If the schema does not declare
attributeas a credential, the sheet opens to an unavailable message telling the user changes are not possible and to contact their administrator, instead of the form. - If the schema declares a
regexfor the attribute, the form enforces it and shows the schema'sdescriptionas the requirement hint, falling back to a generic message when the schema has none. - Each field's own label (e.g. "New Password") is its placeholder, and disappears once the user starts typing. A trailing button toggles the value between masked and plain text.
- Dismissing the sheet clears the entered values; a successful change also clears them and dismisses the sheet automatically.
Customization with BaseChangeCredential
BaseChangeCredential is the unstyled builder variant. It manages the schema lookup, validation, and the network call, and passes the current state to the content closure.
The content closure receives a plain ChangeCredentialState reference, not a property-wrapped one, so a TextField/SecureField two-way binding to it needs a child view that re-wraps it as @ObservedObject, the same shape ChangeCredential's own built-in row and sheet use internally:
import SwiftUI
import ThunderID
import ThunderIDSwiftUI
struct CustomChangePin: View {
var body: some View {
BaseChangeCredential(
attribute: "pin",
onSuccess: { print("PIN updated") }
) { state in
CustomChangePinForm(state: state)
}
}
}
private struct CustomChangePinForm: View {
@ObservedObject var state: ChangeCredentialState
var body: some View {
VStack(alignment: .leading, spacing: 12) {
if let error = state.error {
Text(error).foregroundStyle(.red)
}
SecureField("New \(state.credentialDisplayName)", text: $state.newValue)
.textFieldStyle(.roundedBorder)
SecureField("Confirm New \(state.credentialDisplayName)", text: $state.confirmValue)
.textFieldStyle(.roundedBorder)
Button(state.loading ? "Saving…" : "Save") {
state.submit()
}
.disabled(!state.evaluation.isValid || state.loading)
}
}
}
BaseChangeCredential Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
attribute | String | ❌ | The credential attribute to manage. Defaults to "password". |
credentialDisplayName | String? | ❌ | Overrides the display name resolved from the schema. ChangeCredential does not expose this; it is an escape hatch for callers building their own UI without schema context. |
policyRegex | String? | ❌ | Overrides the schema-derived validation regex. |
onSuccess | (() -> Void)? | ❌ | Called after a successful credential change. |
onError | (() -> Void)? | ❌ | Called when a change fails. |
content | (ChangeCredentialState) -> Content | ✅ | Renders the form from the current state. |
ChangeCredentialState Properties
| Property | Type | Description |
|---|---|---|
credentialDisplayName | String | The resolved display name for the credential. |
newValue | String | The new value entered so far. Bindable. |
confirmValue | String | The re-entered confirmation value. Bindable. |
error | String? | Form-level error from the last failed submission. |
loading | Bool | true while a submission is in flight. |
success | Bool | true after the last submission succeeded. |
unavailable | Bool | true when the schema does not declare attribute as a credential. |
evaluation | CredentialFormEvaluation | Derived validation state: isValid, confirmMatches, meetsPolicy, patternChecked, patternPassed. |
fieldError(_:) | (CredentialField) -> String? | The error for CredentialField.new or CredentialField.form, if any. |
submit() | () -> Void | Submits the current values. No-ops while invalid, loading, or unavailable. |