Skip to content

CryptoOnramp SDK: Example App KYC Step-Up Flow#6217

Merged
mliberatore merged 38 commits into
masterfrom
mliberatore/crypto-onramp-l0-kyc
Mar 18, 2026
Merged

CryptoOnramp SDK: Example App KYC Step-Up Flow#6217
mliberatore merged 38 commits into
masterfrom
mliberatore/crypto-onramp-l0-kyc

Conversation

@mliberatore

@mliberatore mliberatore commented Mar 17, 2026

Copy link
Copy Markdown
Collaborator

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.

  • L0 → L1: We collected L0 fields from the user after auth, but the transaction requires L1. We present the KYC form with only SSN and DOB fields to complete L1.
  • L1 → L2: We collected L0 fields from the user after auth, and potentially later collected L1 fields in the scenario above. However, the transaction requires L2 (identity document verification). We present the Identity flow to complete L2.
  • L0 → L2: We collected L0 fields from the user after auth, but the transaction requires L2. We present the KYC form with only SSN and DOB fields, and then we push the identity flow afterwards to step up to L2.

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_verification with 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_verification with 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_verification with L0 fields attached. (Note that this scenario is simulated, as the backend is returning crypto_onramp_missing_identity_verification incorrectly 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

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
}
}

extension CustomerInformationResponse {

@mliberatore mliberatore Mar 17, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Identifiable was added for the .sheet API used in PaymentView.

@mliberatore mliberatore marked this pull request as ready for review March 17, 2026 17:26
@mliberatore mliberatore requested review from a team as code owners March 17, 2026 17:26
let errorDescription = error.localizedDescription
let fallbackAlert = Alert(title: "Failed to create onramp session", message: errorDescription)

if errorDescription.hasPrefix("HTTP 400"), shouldFetchCustomerInfoForRecovery(from: errorDescription) {

@mliberatore mliberatore Mar 17, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for my own understanding, should we expose these types eventually?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, looks great!

@mliberatore mliberatore requested a review from mats-stripe March 17, 2026 17:29
Base automatically changed from mliberatore/crypto-onramp-l0-kyc-setting-and-initial-routing to master March 17, 2026 21:17
# 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, ); }; };

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
jeanregisser previously approved these changes Mar 17, 2026

@jeanregisser jeanregisser left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for my own understanding, should we expose these types eventually?

}
}
}
.loadingOverlay(isVisible: isLoading.wrappedValue)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: since dismiss() hides the sheet with an animation, could this cause issues if onSuccess() wanted to trigger another sheet?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +79 to +82
// 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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This backend change was resolved, so I removed the temporary workaround and retested.

@mats-stripe mats-stripe left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - I'm generally peaceful with changes to the example app 👍

@mliberatore mliberatore merged commit 040c8dc into master Mar 18, 2026
7 checks passed
@mliberatore mliberatore deleted the mliberatore/crypto-onramp-l0-kyc branch March 18, 2026 18:57
mliberatore added a commit that referenced this pull request Mar 19, 2026
## 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants