Skip to content

Commit 6ee520f

Browse files
authored
feat(ramps): adds sync trigger methods (#7662)
## Explanation <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> Adds sync trigger methods to RampsController for fire-and-forget operations from React effects. ## Problem Controller methods both throw errors AND store them in state. Consumers using these methods in `useEffect` must awkwardly catch and ignore errors since errors are already tracked via selectors: ```typescript useEffect(() => { controller.updateUserRegion().catch(() => {}); // awkward }, []); ``` ## Solution Add sync `trigger*` variants that return `void` and catch errors internally: - `triggerUpdateUserRegion(options)` - `triggerSetUserRegion(region, options)` - `triggerGetCountries(action, options)` - `triggerGetTokens(region, action, options)` - `triggerGetProviders(region, options)` ```typescript useEffect(() => { controller.triggerUpdateUserRegion(); // clean }, []); ``` ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds non-throwing, fire-and-forget APIs for React effects and records errors in state. > > - New methods: `triggerUpdateUserRegion(options)`, `triggerSetUserRegion(region, options)`, `triggerGetCountries(action, options)`, `triggerGetTokens(region, action, options)`, `triggerGetProviders(region, options)` in `RampsController` > - Extensive tests validating void return, state updates, and non-throwing behavior for failures > - `CHANGELOG.md` updated under Unreleased > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6d53b2a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent a7c0812 commit 6ee520f

File tree

3 files changed

+259
-0
lines changed

3 files changed

+259
-0
lines changed

packages/ramps-controller/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Add sync trigger methods to RampsController ([#7662](https://github.com/MetaMask/core/pull/7662))
13+
1214
- Export `RampAction` type for `'buy' | 'sell'` ramp actions ([#7663](https://github.com/MetaMask/core/pull/7663))
1315

1416
## [4.0.0]

packages/ramps-controller/src/RampsController.test.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,182 @@ describe('RampsController', () => {
935935
});
936936
});
937937

938+
describe('sync trigger methods', () => {
939+
describe('triggerUpdateUserRegion', () => {
940+
it('triggers user region update and returns void', async () => {
941+
await withController(async ({ controller, rootMessenger }) => {
942+
rootMessenger.registerActionHandler(
943+
'RampsService:getGeolocation',
944+
async () => 'us',
945+
);
946+
rootMessenger.registerActionHandler(
947+
'RampsService:getCountries',
948+
async () => createMockCountries(),
949+
);
950+
rootMessenger.registerActionHandler(
951+
'RampsService:getProviders',
952+
async () => ({ providers: [] }),
953+
);
954+
955+
const result = controller.triggerUpdateUserRegion();
956+
expect(result).toBeUndefined();
957+
958+
await new Promise((resolve) => setTimeout(resolve, 10));
959+
expect(controller.state.userRegion?.regionCode).toBe('us');
960+
});
961+
});
962+
963+
it('does not throw when update fails', async () => {
964+
await withController(async ({ controller, rootMessenger }) => {
965+
rootMessenger.registerActionHandler(
966+
'RampsService:getGeolocation',
967+
async () => {
968+
throw new Error('geolocation failed');
969+
},
970+
);
971+
972+
expect(() => controller.triggerUpdateUserRegion()).not.toThrow();
973+
});
974+
});
975+
});
976+
977+
describe('triggerSetUserRegion', () => {
978+
it('triggers set user region and returns void', async () => {
979+
await withController(async ({ controller, rootMessenger }) => {
980+
rootMessenger.registerActionHandler(
981+
'RampsService:getCountries',
982+
async () => createMockCountries(),
983+
);
984+
rootMessenger.registerActionHandler(
985+
'RampsService:getProviders',
986+
async () => ({ providers: [] }),
987+
);
988+
989+
const result = controller.triggerSetUserRegion('us');
990+
expect(result).toBeUndefined();
991+
992+
await new Promise((resolve) => setTimeout(resolve, 10));
993+
expect(controller.state.userRegion?.regionCode).toBe('us');
994+
});
995+
});
996+
997+
it('does not throw when set fails', async () => {
998+
await withController(async ({ controller, rootMessenger }) => {
999+
rootMessenger.registerActionHandler(
1000+
'RampsService:getCountries',
1001+
async () => {
1002+
throw new Error('countries failed');
1003+
},
1004+
);
1005+
1006+
expect(() => controller.triggerSetUserRegion('us')).not.toThrow();
1007+
});
1008+
});
1009+
});
1010+
1011+
describe('triggerGetCountries', () => {
1012+
it('triggers get countries and returns void', async () => {
1013+
await withController(async ({ controller, rootMessenger }) => {
1014+
rootMessenger.registerActionHandler(
1015+
'RampsService:getCountries',
1016+
async () => createMockCountries(),
1017+
);
1018+
1019+
const result = controller.triggerGetCountries('buy');
1020+
expect(result).toBeUndefined();
1021+
});
1022+
});
1023+
1024+
it('does not throw when fetch fails', async () => {
1025+
await withController(async ({ controller, rootMessenger }) => {
1026+
rootMessenger.registerActionHandler(
1027+
'RampsService:getCountries',
1028+
async () => {
1029+
throw new Error('countries failed');
1030+
},
1031+
);
1032+
1033+
expect(() => controller.triggerGetCountries()).not.toThrow();
1034+
});
1035+
});
1036+
});
1037+
1038+
describe('triggerGetTokens', () => {
1039+
it('triggers get tokens and returns void', async () => {
1040+
await withController(
1041+
{ options: { state: { userRegion: createMockUserRegion('us') } } },
1042+
async ({ controller, rootMessenger }) => {
1043+
rootMessenger.registerActionHandler(
1044+
'RampsService:getTokens',
1045+
async () => ({ topTokens: [], allTokens: [] }),
1046+
);
1047+
1048+
const result = controller.triggerGetTokens();
1049+
expect(result).toBeUndefined();
1050+
1051+
await new Promise((resolve) => setTimeout(resolve, 10));
1052+
expect(controller.state.tokens).toStrictEqual({
1053+
topTokens: [],
1054+
allTokens: [],
1055+
});
1056+
},
1057+
);
1058+
});
1059+
1060+
it('does not throw when fetch fails', async () => {
1061+
await withController(
1062+
{ options: { state: { userRegion: createMockUserRegion('us') } } },
1063+
async ({ controller, rootMessenger }) => {
1064+
rootMessenger.registerActionHandler(
1065+
'RampsService:getTokens',
1066+
async () => {
1067+
throw new Error('tokens failed');
1068+
},
1069+
);
1070+
1071+
expect(() => controller.triggerGetTokens()).not.toThrow();
1072+
},
1073+
);
1074+
});
1075+
});
1076+
1077+
describe('triggerGetProviders', () => {
1078+
it('triggers get providers and returns void', async () => {
1079+
await withController(
1080+
{ options: { state: { userRegion: createMockUserRegion('us') } } },
1081+
async ({ controller, rootMessenger }) => {
1082+
rootMessenger.registerActionHandler(
1083+
'RampsService:getProviders',
1084+
async () => ({ providers: [] }),
1085+
);
1086+
1087+
const result = controller.triggerGetProviders();
1088+
expect(result).toBeUndefined();
1089+
1090+
await new Promise((resolve) => setTimeout(resolve, 10));
1091+
expect(controller.state.providers).toStrictEqual([]);
1092+
},
1093+
);
1094+
});
1095+
1096+
it('does not throw when fetch fails', async () => {
1097+
await withController(
1098+
{ options: { state: { userRegion: createMockUserRegion('us') } } },
1099+
async ({ controller, rootMessenger }) => {
1100+
rootMessenger.registerActionHandler(
1101+
'RampsService:getProviders',
1102+
async () => {
1103+
throw new Error('providers failed');
1104+
},
1105+
);
1106+
1107+
expect(() => controller.triggerGetProviders()).not.toThrow();
1108+
},
1109+
);
1110+
});
1111+
});
1112+
});
1113+
9381114
describe('getCountries', () => {
9391115
const mockCountries: Country[] = [
9401116
{

packages/ramps-controller/src/RampsController.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,4 +842,85 @@ export class RampsController extends BaseController<
842842

843843
return { providers };
844844
}
845+
846+
// ============================================================
847+
// Sync Trigger Methods
848+
// These fire-and-forget methods are for use in React effects.
849+
// Errors are stored in state and available via selectors.
850+
// ============================================================
851+
852+
/**
853+
* Triggers a user region update without throwing.
854+
*
855+
* @param options - Options for cache behavior.
856+
*/
857+
triggerUpdateUserRegion(options?: ExecuteRequestOptions): void {
858+
this.updateUserRegion(options).catch(() => {
859+
// Error stored in state
860+
});
861+
}
862+
863+
/**
864+
* Triggers setting the user region without throwing.
865+
*
866+
* @param region - The region code to set (e.g., "US-CA").
867+
* @param options - Options for cache behavior.
868+
*/
869+
triggerSetUserRegion(region: string, options?: ExecuteRequestOptions): void {
870+
this.setUserRegion(region, options).catch(() => {
871+
// Error stored in state
872+
});
873+
}
874+
875+
/**
876+
* Triggers fetching countries without throwing.
877+
*
878+
* @param action - The ramp action type ('buy' or 'sell').
879+
* @param options - Options for cache behavior.
880+
*/
881+
triggerGetCountries(
882+
action: 'buy' | 'sell' = 'buy',
883+
options?: ExecuteRequestOptions,
884+
): void {
885+
this.getCountries(action, options).catch(() => {
886+
// Error stored in state
887+
});
888+
}
889+
890+
/**
891+
* Triggers fetching tokens without throwing.
892+
*
893+
* @param region - The region code. If not provided, uses userRegion from state.
894+
* @param action - The ramp action type ('buy' or 'sell').
895+
* @param options - Options for cache behavior.
896+
*/
897+
triggerGetTokens(
898+
region?: string,
899+
action: 'buy' | 'sell' = 'buy',
900+
options?: ExecuteRequestOptions,
901+
): void {
902+
this.getTokens(region, action, options).catch(() => {
903+
// Error stored in state
904+
});
905+
}
906+
907+
/**
908+
* Triggers fetching providers without throwing.
909+
*
910+
* @param region - The region code. If not provided, uses userRegion from state.
911+
* @param options - Options for cache behavior and query filters.
912+
*/
913+
triggerGetProviders(
914+
region?: string,
915+
options?: ExecuteRequestOptions & {
916+
provider?: string | string[];
917+
crypto?: string | string[];
918+
fiat?: string | string[];
919+
payments?: string | string[];
920+
},
921+
): void {
922+
this.getProviders(region, options).catch(() => {
923+
// Error stored in state
924+
});
925+
}
845926
}

0 commit comments

Comments
 (0)