Skip to content

Commit 3bb1ee9

Browse files
committed
chore(runway): cherry-pick fix: start Ramps V2 init when remote feature flags hydrate cp-7.71.0 (#27778)
## **Description** On a fresh install, `RemoteFeatureFlagController` loads flags asynchronously while Engine builds controllers. `rampsControllerInit` previously read the unified buy V2 flag only once; if flags were not in state yet, `RampsController.init()` never ran, so buy token lists stayed empty until a full app restart. This change subscribes to `RemoteFeatureFlagController:stateChange` (already delegated on `RampsControllerInitMessenger`) and re-runs the same V2 startup path when remote flag state updates. Order-status subscriptions are registered at most once. `RampsController.init()` remains idempotent for repeated calls. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TRAM-3350 ## **Manual testing steps** ```gherkin Feature: Unified buy V2 after fresh install Scenario: Buy token list loads without restarting the app Given a dev build with unified buy V2 enabled via remote flags And the app is installed fresh (or remote flag cache cleared) When the user completes onboarding and opens Buy / token selection Then tokens and providers load without requiring an app restart ``` ## **Screenshots/Recordings** <div> <a href="https://www.loom.com/share/e80a3794612d4030aa963834e5a8d7bf"> <p>Fix Ramps controller not initializing on fresh install - Watch Video</p> </a> <a href="https://www.loom.com/share/e80a3794612d4030aa963834e5a8d7bf"> <img style="max-width:300px;" src="https://cdn.loom.com/sessions/thumbnails/e80a3794612d4030aa963834e5a8d7bf-13202ae1a2494608-full-play.gif#t=0.1"> </a> </div> ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds a new subscription-driven initialization path triggered by `RemoteFeatureFlagController:stateChange`, which can change startup behavior and potentially cause repeated init/polling if underlying idempotency assumptions are wrong. > > **Overview** > Ensures Unified Buy V2 startup runs even when remote feature flags hydrate *after* Engine/controller initialization by subscribing to `RemoteFeatureFlagController:stateChange` and re-checking the V2 flag. > > Refactors V2 startup into a helper that conditionally calls `RampsController.init()`/`startOrderPolling()` and registers order-status subscriptions only once. Updates tests to cover the “flag off at startup then enabled on stateChange” scenario and to include `subscribe` in the thrown-state mock. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 78ff800. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 445b430 commit 3bb1ee9

2 files changed

Lines changed: 62 additions & 8 deletions

File tree

app/core/Engine/controllers/ramps-controller/ramps-controller-init.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,40 @@ describe('ramps controller init', () => {
207207
});
208208
});
209209

210+
it('calls init when remote flags were off at startup then V2 enables on RemoteFeatureFlagController:stateChange', async () => {
211+
let remoteEnabled = false;
212+
const subscribeMock = jest.fn();
213+
const initMessenger = {
214+
call: jest.fn(() => ({
215+
remoteFeatureFlags: {
216+
rampsUnifiedBuyV2: remoteEnabled
217+
? { enabled: true, minimumVersion: '1.0.0' }
218+
: { enabled: false },
219+
},
220+
})),
221+
subscribe: subscribeMock,
222+
} as unknown as RampsControllerInitMessenger;
223+
224+
initRequestMock.initMessenger = initMessenger;
225+
226+
rampsControllerInit(initRequestMock);
227+
228+
expect(mockInit).not.toHaveBeenCalled();
229+
230+
const stateChangeHandler = subscribeMock.mock.calls.find(
231+
(call) => call[0] === 'RemoteFeatureFlagController:stateChange',
232+
)?.[1] as () => void;
233+
234+
expect(stateChangeHandler).toBeDefined();
235+
236+
remoteEnabled = true;
237+
stateChangeHandler();
238+
239+
await waitFor(() => {
240+
expect(mockInit).toHaveBeenCalledTimes(1);
241+
});
242+
});
243+
210244
it('handles init failure gracefully', async () => {
211245
initRequestMock.initMessenger = createMockInitMessenger({
212246
enabled: true,
@@ -253,6 +287,7 @@ describe('ramps controller init', () => {
253287
call: jest.fn().mockImplementation(() => {
254288
throw new Error('Controller not ready');
255289
}),
290+
subscribe: jest.fn(),
256291
} as unknown as RampsControllerInitMessenger;
257292

258293
rampsControllerInit(initRequestMock);

app/core/Engine/controllers/ramps-controller/ramps-controller-init.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import { handleOrderStatusChangedForNotifications } from './event-handlers/notif
1111
import { handleOrderStatusChangedForMetrics } from './event-handlers/analytics';
1212

1313
/**
14-
* Determines whether the ramps unified buy V2 feature is enabled
15-
* by reading the remote feature flag state.
14+
* Whether Unified Buy V2 is enabled per RemoteFeatureFlagController state.
1615
*
1716
* @param initMessenger - The init messenger to read RemoteFeatureFlagController state.
1817
* @returns Whether V2 is enabled.
@@ -54,21 +53,28 @@ export const rampsControllerInit: ControllerInitFunction<
5453
state: rampsControllerState,
5554
});
5655

57-
const isV2Enabled = getIsRampsUnifiedBuyV2Enabled(initMessenger);
56+
let orderSubscriptionsRegistered = false;
5857

59-
if (isV2Enabled) {
58+
const registerUnifiedBuyV2OrderSubscriptions = (): void => {
59+
if (orderSubscriptionsRegistered) {
60+
return;
61+
}
62+
orderSubscriptionsRegistered = true;
6063
initMessenger.subscribe(
6164
'RampsController:orderStatusChanged',
6265
handleOrderStatusChangedForNotifications,
6366
);
64-
6567
initMessenger.subscribe(
6668
'RampsController:orderStatusChanged',
6769
handleOrderStatusChangedForMetrics,
6870
);
71+
};
6972

70-
// Start init immediately so tokens (and providers) load on app start.
71-
// init() is async and does not block controller creation.
73+
const startUnifiedBuyV2IfEnabled = (): void => {
74+
if (!getIsRampsUnifiedBuyV2Enabled(initMessenger)) {
75+
return;
76+
}
77+
registerUnifiedBuyV2OrderSubscriptions();
7278
controller
7379
.init()
7480
.then(() => {
@@ -77,7 +83,20 @@ export const rampsControllerInit: ControllerInitFunction<
7783
.catch(() => {
7884
// Initialization failed - error state will be available via selectors
7985
});
80-
}
86+
};
87+
88+
startUnifiedBuyV2IfEnabled();
89+
90+
// Remote flags can be empty on first Engine init and fill in once the
91+
// controller has fetched; re-check so RampsController.init() runs then.
92+
//
93+
// This event fires for any RemoteFeatureFlagController state update — not
94+
// only rampsUnifiedBuyV2. When V2 is off, startUnifiedBuyV2IfEnabled returns
95+
// immediately. When V2 is on, order subscriptions register once; init() and
96+
// startOrderPolling() are idempotent, so repeat invocations are safe.
97+
initMessenger.subscribe('RemoteFeatureFlagController:stateChange', () => {
98+
startUnifiedBuyV2IfEnabled();
99+
});
81100

82101
return {
83102
controller,

0 commit comments

Comments
 (0)