CryptoOnramp SDK: Example App KYC Step-Up Flow#6217
Conversation
…ry error handling
…mliberatore/crypto-onramp-l0-kyc
This helps to determine if we’re at level 1 when the mode is level 0 (i.e. the user chose to fill the optional SSN + DOB fields).
…uting' into mliberatore/crypto-onramp-l0-kyc # Conflicts: # Example/CryptoOnramp Example/CryptoOnramp Example/CryptoOnrampExampleView.swift # Example/CryptoOnramp Example/CryptoOnramp Example/CryptoOnrampFlowCoordinator.swift # Example/CryptoOnramp Example/CryptoOnramp Example/KYCLevel.swift
… various scenarios
| } | ||
| } | ||
|
|
||
| extension CustomerInformationResponse { |
There was a problem hiding this comment.
These changes were moved (and expanded upon) to an extension in the same file as the type (above), since we make use of these convenience properties from PaymentView as well now.
|
|
||
| /// Represents progressively stronger customer verification completeness. | ||
| enum KYCLevel { | ||
| enum KYCLevel: String, Identifiable { |
There was a problem hiding this comment.
Identifiable was added for the .sheet API used in PaymentView.
| let errorDescription = error.localizedDescription | ||
| let fallbackAlert = Alert(title: "Failed to create onramp session", message: errorDescription) | ||
|
|
||
| if errorDescription.hasPrefix("HTTP 400"), shouldFetchCustomerInfoForRecovery(from: errorDescription) { |
There was a problem hiding this comment.
Note that the StripeError / StripeAPIError types are not exposed publicly, so our error identification is a bit rudimentary here in the demo app, matching the HTTP status, and then searching the body for the error code from the error description.
There was a problem hiding this comment.
Just for my own understanding, should we expose these types eventually?
There was a problem hiding this comment.
Not to my knowledge. They live outside of our framework, and are Stripe private types.
However, thank you for bringing this up. When beginning to explain more about StripeError and how we propagate errors back to the client, I realized that since this is using our example app's API client, not Stripe's, we're in complete control of the error type returned.
Will refactor this now to parse the error information more appropraitely.
There was a problem hiding this comment.
Updated. The error handling here is much more robust, with JSON-parsed error bodies → associated values in our custom error type.
(lldb) po code
crypto_onramp_missing_identity_verification
(lldb) po error
▿ APIError
▿ httpError : 3 elements
- status : 400
- message : "Identity verification is required for this transaction."
▿ code : Optional<String>
- some : "crypto_onramp_missing_identity_verification"
# Conflicts: # Example/CryptoOnramp Example/CryptoOnramp Example/CryptoOnrampFlowCoordinator.swift # Example/CryptoOnramp Example/CryptoOnramp Example/KYCLevel.swift
| 0B44B40C2E57702B00B58655 /* StripeCameraCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B44B40B2E57702B00B58655 /* StripeCameraCore.framework */; }; | ||
| 0B44B40D2E57702B00B58655 /* StripeCameraCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0B44B40B2E57702B00B58655 /* StripeCameraCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; | ||
| 0B8EB14A2F68947200D3C114 /* StripeIssuing.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B8EB1492F68947200D3C114 /* StripeIssuing.framework */; }; | ||
| 0B8EB14B2F68947200D3C114 /* StripeIssuing.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0B8EB1492F68947200D3C114 /* StripeIssuing.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; |
There was a problem hiding this comment.
The lack of inclusion of this framework was causing device builds to fail in Xcode. I’ve updated the README as well to now include this.
jeanregisser
left a comment
There was a problem hiding this comment.
LGTM 👍
cc @mats-stripe since I don't have much context yet.
| let errorDescription = error.localizedDescription | ||
| let fallbackAlert = Alert(title: "Failed to create onramp session", message: errorDescription) | ||
|
|
||
| if errorDescription.hasPrefix("HTTP 400"), shouldFetchCustomerInfoForRecovery(from: errorDescription) { |
There was a problem hiding this comment.
Just for my own understanding, should we expose these types eventually?
| } | ||
| } | ||
| } | ||
| .loadingOverlay(isVisible: isLoading.wrappedValue) |
There was a problem hiding this comment.
Non-blocking UX thought: do we want to avoid a full-screen loading overlay during async operations here? It can feel pretty heavy since it removes context and blocks the whole screen, whereas inline progress or disabling only the relevant action might feel smoother. Also, since this is an example app, there’s some risk that integrators take this as a recommended pattern. Totally fine if there’s context I’m missing.
There was a problem hiding this comment.
That's great feedback. Thus far, integration of the SDK has been to existing apps, but I do want to make sure we're putting best practices/patterns forward for folks in need of more context and assistance when building from scratch.
Since this would be a sweeping change (noting that we're using this loading modal across the entire example app experience), it will make sense to tackle this in isolation from these changes. We have a few priorities left on this feature, but we can chat more in our sync about these improvements and plan them out. Some Views might be trickier, with a lot of potential user interaction, and not-yet a good solution for cancelation of operations (if we were to leave the UI enabled) so I think it will make sense to think this through holistically.
|
|
||
| private func finish() { | ||
| dismiss() | ||
| onSuccess() |
There was a problem hiding this comment.
nit: since dismiss() hides the sheet with an animation, could this cause issues if onSuccess() wanted to trigger another sheet?
There was a problem hiding this comment.
I‘m not experiencing an issue with sequential presentation in practice here. We currently present an alert upon dismissal in onSuccess(). To be more certain, I’ve wired up a test sheet to confirm an issue does not occur, and confirmed there's no console logs to suggest an issue with presentation occurring during the dismissal, nor a second presentation occurring while already presenting.
Presentation-after-kyc-flow-dismissal.mov
| // Note that the `isKycVerified` check is currently disabled. | ||
| // We're waiting on a backend change that properly reports `verified` when a user | ||
| // has collected at least L0. Instead, right now it reports `not_started`. | ||
| guard providedKYCLevel.includesLevel0, isPhoneVerified /*, isKycVerified*/ else { |
There was a problem hiding this comment.
This backend change was resolved, so I removed the temporary workaround and retested.
mats-stripe
left a comment
There was a problem hiding this comment.
LGTM - I'm generally peaceful with changes to the example app 👍
## Summary This makes two minor changes that were requested for testing out L0 KYC Mode 1. `livemode` is no longer required to be enabled to select L0 KYC Mode. While onramp session creation will fail in test mode when providing only L0 KYC info, this was requested to assist in testing the flow. 2. Removes the requirement of `phone_verified` status when determining KYC level. ## Motivation Requests from Xuhui in Lickability slack: https://lickability.slack.com/archives/C094P3BRBL1/p1773937514217369?thread_ts=1773760441.342059&cid=C094P3BRBL1 (To open in Stripe slack, paste/send this link to Slackbot, and it should convert it appropriately.) ## Testing Repeated the tests from #6217, without the need to apply a local workaround to enable L0 KYC Mode in test mode. ## Changelog <!-- Is this a notable change that affects users? If so, add a line to `CHANGELOG.md` and prefix the line with one of the following: - [Added] for new features. - [Changed] for changes in existing functionality. - [Deprecated] for soon-to-be removed features. - [Removed] for now removed features. - [Fixed] for any bug fixes. - [Security] in case of vulnerabilities. --> N/A, Alpha SDK and these changes only affect the example app, not the SDK.
Summary
Adds step-up flow logic to the demo app for errors encountered related to expected L0 KYC-related errors.
When creating an onramp session, we respond to certain error codes returned from the backend and collect more KYC information as needed.
Motivation
https://docs.google.com/document/d/1PAjI3bl3S_h3630TpZ012-wn4k6lQJlFyxaX3cmTRhE/edit?tab=t.0#heading=h.lnqfcyy21mit
Testing
Tested the step-up flow by disabling the livemode requirement, and enabling the L0 KYC mode introduced in #6198, then getting to the payment screen, and attempting to create onramp sessions. Note that checkout will fail in livemode under these conditions, but starting the onramp session at least allows us to respond to appropriate errors when we provide large transaction $$ amounts.
Level 0 → Level 1 step-up, in response to
crypto_onramp_missing_identity_verificationwith L0 fields attached:Simulator.Screen.Recording.-.iPhone.17.-.2026-03-17.at.12.54.14.mov
Level 1 → Level 2 step-up, in response to
crypto_onramp_missing_document_verificationwith L1 fields attached:Simulator.Screen.Recording.-.iPhone.17.-.2026-03-17.at.12.55.00.mov
Level 0 → L2 step-up, in response to
crypto_onramp_missing_document_verificationwith L0 fields attached. (Note that this scenario is simulated, as the backend is returningcrypto_onramp_missing_identity_verificationincorrectly at the moment. I swapped the error check to force this scenario to test the 2-step step-up process.):Simulator.Screen.Recording.-.iPhone.17.-.2026-03-17.at.12.59.24.mov
Changelog
N/A, SDK is in Alpha