Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/kyc-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **BREAKING:** `KycController.checkKycRequired` and the consents-path session (`acceptTermsAndStartSession` / a resumed `initialize`) now rethrow the underlying error after recording it on controller state. ([#10214](https://github.com/MetaMask/core/pull/10214))
- A product-scoped MoonPay auto-run therefore rejects `handleFrameMessage` / `onAuthenticated` when the KYC-required check fails, instead of only setting `phase: 'error'`.
- Bump `@metamask/profile-sync-controller` from `^32.1.0` to `^32.1.1` ([#10220](https://github.com/MetaMask/core/pull/10220))

## [0.3.0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import type { KycController } from './KycController.js';
* call `checkKycRequired` manually.
* @param params.vendor - Identity vendor for this flow. Non-MoonPay vendors
* skip Check/Auth frames and use the consents path. Defaults to `moonpay`.
* @throws If a resumed consents-path session records a failure on state.
* The original error is rethrown after the controller rewinds to `terms`.
*/
export type KycControllerInitializeAction = {
type: `KycController:initialize`;
Expand Down Expand Up @@ -94,6 +96,8 @@ export type KycControllerFetchSessionDisclaimersAction = {
* @param params.credentialReusabilityConsentGiven - Whether the customer
* consented to reuse existing idOS credentials. Used when recording
* session-scoped disclaimers on the consents path. Defaults to `false`.
* @throws If the consents-path session records a failure on state. The
* original error is rethrown after the controller rewinds to `terms`.
*/
export type KycControllerAcceptTermsAndStartSessionAction = {
type: `KycController:acceptTermsAndStartSession`;
Expand Down Expand Up @@ -160,6 +164,8 @@ export type KycControllerBuildResetFrameUrlAction = {
* @param params.product - The consuming feature.
* @param params.country - Optional alpha-3 country override.
* @returns Whether KYC is required.
* @throws If the KYC-required service call fails after the error is recorded
* on controller state (`phase: 'error'`).
*/
export type KycControllerCheckKycRequiredAction = {
type: `KycController:checkKycRequired`;
Expand Down
164 changes: 97 additions & 67 deletions packages/kyc-controller/src/KycController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -943,11 +943,13 @@ describe('KycController', () => {
async ({ controller, handlers }) => {
handlers.submitVendorDisclaimers.mockRejectedValue(new Error('stop'));

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
});
await expect(
controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
}),
).rejects.toThrow('stop');

expect(handlers.submitVendorDisclaimers).toHaveBeenCalledWith({
vendor: 'iron',
Expand All @@ -970,11 +972,13 @@ describe('KycController', () => {
async ({ controller, handlers }) => {
handlers.submitVendorDisclaimers.mockRejectedValue(new Error('down'));

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
});
await expect(
controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
}),
).rejects.toThrow('down');

expect(controller.state.vendorDisclaimersAccepted.iron).toBeNull();
},
Expand Down Expand Up @@ -1162,7 +1166,9 @@ describe('KycController', () => {
async ({ controller, handlers, launcher, moonPayFrames }) => {
handlers.checkKycRequired.mockRejectedValue(new Error('down'));

await moonPayFrames.options.onAuthenticated();
await expect(moonPayFrames.options.onAuthenticated()).rejects.toThrow(
'down',
);

expect(controller.state.phase).toBe('error');
expect(launcher.launch).not.toHaveBeenCalled();
Expand Down Expand Up @@ -1293,9 +1299,9 @@ describe('KycController', () => {
async ({ controller, handlers }) => {
handlers.checkKycRequired.mockRejectedValue(new Error('down'));

expect(await controller.checkKycRequired({ product: 'ramps' })).toBe(
false,
);
await expect(
controller.checkKycRequired({ product: 'ramps' }),
).rejects.toThrow('down');
expect(controller.state.error).toMatch(/KYC check failed/u);
},
);
Expand Down Expand Up @@ -3406,13 +3412,15 @@ describe('KycController', () => {
);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
product: 'money',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
credentialReusabilityConsentGiven: true,
});
await expect(
controller.acceptTermsAndStartSession({
email: 'a@b.co',
product: 'money',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
credentialReusabilityConsentGiven: true,
}),
).rejects.toThrow("Fetching 'disclaimers' failed with status '409'");

expect(controller.state.phase).toBe('terms');
expect(controller.state.error).toMatch(/Consents session failed/u);
Expand Down Expand Up @@ -3443,12 +3451,14 @@ describe('KycController', () => {
);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
product: 'money',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
});
await expect(
controller.acceptTermsAndStartSession({
email: 'a@b.co',
product: 'money',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
}),
).rejects.toThrow("Fetching 'disclaimers' failed with status '409'");

expect(controller.state.phase).toBe('terms');
expect(controller.state.error).toMatch(/Consents session failed/u);
Expand Down Expand Up @@ -3710,11 +3720,13 @@ describe('KycController', () => {
);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
});
await expect(
controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
}),
).rejects.toThrow('sumsub down');

expect(controller.state.phase).toBe('terms');
expect(controller.state.vendorDisclaimersAccepted.iron).toBeNull();
Expand All @@ -3737,11 +3749,13 @@ describe('KycController', () => {
handlers.createJourney.mockRejectedValue(new Error('journey down'));
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
});
await expect(
controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
}),
).rejects.toThrow('journey down');

expect(controller.state.phase).toBe('terms');
expect(controller.state.error).toMatch(/journey down/u);
Expand Down Expand Up @@ -3810,11 +3824,13 @@ describe('KycController', () => {
});
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
});
await expect(
controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
}),
).rejects.toThrow('SumSub verification could not run.');

expect(controller.state.phase).toBe('terms');
expect(controller.state.sumsub.status).toBe('idle');
Expand Down Expand Up @@ -4213,11 +4229,13 @@ describe('KycController', () => {
);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
});
await expect(
controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
}),
).rejects.toThrow('iron signings down');

expect(controller.state.phase).toBe('terms');
expect(controller.state.error).toMatch(/iron signings down/u);
Expand Down Expand Up @@ -4275,11 +4293,13 @@ describe('KycController', () => {
);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
});
await expect(
controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
}),
).rejects.toThrow("Fetching 'disclaimers' failed with status '500'");

expect(controller.state.phase).toBe('terms');
expect(controller.state.error).toMatch(/Consents session failed/u);
Expand Down Expand Up @@ -4308,11 +4328,15 @@ describe('KycController', () => {
});
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
});
await expect(
controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
}),
).rejects.toThrow(
'Session disclaimer catalog is missing documents for an accepted category.',
);

expect(controller.state.phase).toBe('terms');
expect(controller.state.error).toMatch(
Expand Down Expand Up @@ -4343,11 +4367,15 @@ describe('KycController', () => {
});
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
});
await expect(
controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
}),
).rejects.toThrow(
'Session disclaimer catalog is missing documents for an accepted category.',
);

expect(controller.state.phase).toBe('terms');
expect(controller.state.error).toMatch(
Expand Down Expand Up @@ -4388,11 +4416,13 @@ describe('KycController', () => {
);
handlers.fetchVendorDisclaimers.mockResolvedValue([]);

await controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
});
await expect(
controller.acceptTermsAndStartSession({
email: 'a@b.co',
providerDisclaimersAccepted: MOCK_SUMSUB_DISCLAIMERS_ACCEPTED,
idosDisclaimersAccepted: MOCK_IDOS_DISCLAIMERS_ACCEPTED,
}),
).rejects.toThrow("Fetching 'disclaimers' failed with status '409'");

expect(controller.state.phase).toBe('terms');
expect(controller.state.error).toMatch(/Consents session failed/u);
Expand Down
18 changes: 14 additions & 4 deletions packages/kyc-controller/src/KycController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,8 @@ export class KycController extends BaseController<
* call `checkKycRequired` manually.
* @param params.vendor - Identity vendor for this flow. Non-MoonPay vendors
* skip Check/Auth frames and use the consents path. Defaults to `moonpay`.
* @throws If a resumed consents-path session records a failure on state.
* The original error is rethrown after the controller rewinds to `terms`.
*/
async initialize(params?: {
email?: string;
Expand Down Expand Up @@ -1167,6 +1169,8 @@ export class KycController extends BaseController<
* @param params.credentialReusabilityConsentGiven - Whether the customer
* consented to reuse existing idOS credentials. Used when recording
* session-scoped disclaimers on the consents path. Defaults to `false`.
* @throws If the consents-path session records a failure on state. The
* original error is rethrown after the controller rewinds to `terms`.
*/
async acceptTermsAndStartSession(params?: {
email?: string;
Expand Down Expand Up @@ -1232,6 +1236,8 @@ export class KycController extends BaseController<
* @param consents.idosDisclaimersAccepted - Accepted idOS disclaimer records.
* @param consents.credentialReusabilityConsentGiven - Whether credential
* reuse was accepted.
* @throws If a step after recording state fails. The original error is
* rethrown after the controller rewinds to `terms`.
*/
async #startConsentsSession(consents: {
providerDisclaimersAccepted: KycConsentRecord[];
Expand Down Expand Up @@ -1377,6 +1383,7 @@ export class KycController extends BaseController<
statusMessage:
'Consent / verification failed — accept the terms to try again.',
});
throw error;
}
}

Expand Down Expand Up @@ -1666,9 +1673,10 @@ export class KycController extends BaseController<
* document-verification sub-flow is launched. When no product is set, this is
* a no-op and the flow stays at `form` for the consumer to drive manually.
*
* Errors are already recorded on state by `checkKycRequired` (`error`
* phase) and `startSumSub` (`sumsub.status = 'failed'`); this method swallows
* them so it can be awaited safely from the frame-message handler.
* `startSumSub` records `sumsub.status = 'failed'` and this method swallows
* that rejection so it can be awaited safely from the frame-message handler.
* `checkKycRequired` still records `phase: 'error'` and rethrows, so a
* failed auto-run check surfaces to the caller.
*/
async #continueAfterAuthentication(): Promise<void> {
const product = this.state.activeProduct;
Expand Down Expand Up @@ -1745,6 +1753,8 @@ export class KycController extends BaseController<
* @param params.product - The consuming feature.
* @param params.country - Optional alpha-3 country override.
* @returns Whether KYC is required.
* @throws If the KYC-required service call fails after the error is recorded
* on controller state (`phase: 'error'`).
*/
async checkKycRequired(params: {
product: KycProduct;
Expand Down Expand Up @@ -1798,7 +1808,7 @@ export class KycController extends BaseController<
return false;
}
this.#fail(`KYC check failed: ${String(error)}`);
return false;
throw error;
}
}

Expand Down