@grabjs/superapp-sdk 2.0.0-beta.46 → 2.0.0-beta.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/skills/SKILL.md CHANGED
@@ -101,17 +101,88 @@ The SDK uses HTTP-style status codes for all responses:
101
101
  | `500` | Internal Error | Unexpected SDK error |
102
102
  | `501` | Not Implemented | Outside Grab SuperApp environment |
103
103
 
104
- ### Handling 403 Forbidden
104
+ ### Type Guards
105
105
 
106
- Methods tagged with `@requiredOAuthScope` require specific permissions. If the user hasn't granted the required scope, the method returns `403`. You must request authorization and reload scopes before retrying:
106
+ Type guards narrow the response type so TypeScript knows which fields are available:
107
107
 
108
- 1. Call `IdentityModule.authorize()` to request the scope.
109
- 2. Call `ScopeModule.reloadScopes()` to refresh the SDK's internal permission state.
110
- 3. Retry the original method call.
108
+ | Guard | Matches |
109
+ | --------------------------------- | ---------------------------------------- |
110
+ | `isSuccess(r)` | `200`, `204` |
111
+ | `isOk(r)` | `200` |
112
+ | `isNoContent(r)` | `204` |
113
+ | `isRedirection(r)` / `isFound(r)` | `302` |
114
+ | `isClientError(r)` | `400`, `401`, `403`, `404`, `424`, `426` |
115
+ | `isServerError(r)` | `500`, `501` |
116
+ | `isError(r)` | any 4xx or 5xx |
111
117
 
112
- #### Proactive Permission Checking
118
+ ```typescript
119
+ import { isSuccess, isOk, isNoContent, isError } from '@grabjs/superapp-sdk';
113
120
 
114
- Proactively verify if the current session has the necessary permissions for a method using `ScopeModule.hasAccessTo()`:
121
+ if (isSuccess(response)) {
122
+ // narrow further if needed
123
+ if (isOk(response)) console.log(response.result);
124
+ if (isNoContent(response)) console.log('done, no data');
125
+ }
126
+
127
+ if (isError(response)) {
128
+ // response.error: string is guaranteed here
129
+ console.error(response.error);
130
+ }
131
+ ```
132
+
133
+ ### Streams
134
+
135
+ Some modules provide streaming methods for real-time data (location updates, media events). Subscribe to receive values over time:
136
+
137
+ ```typescript
138
+ import { LocationModule, isSuccess } from '@grabjs/superapp-sdk';
139
+
140
+ const location = new LocationModule();
141
+
142
+ const subscription = location.observeLocationChange().subscribe({
143
+ next: (response) => {
144
+ if (isSuccess(response)) console.log(response.result);
145
+ },
146
+ complete: () => console.log('Stream ended'),
147
+ });
148
+
149
+ // Always unsubscribe when done to conserve battery and resources
150
+ subscription.unsubscribe();
151
+ ```
152
+
153
+ You can also `await` a stream method directly to get its first value.
154
+
155
+ ### Scopes and Permissions
156
+
157
+ The SDK categorizes permissions into two distinct types based on their execution context:
158
+
159
+ #### Permission Types
160
+
161
+ - **Backend Scopes** (`openid`, `profile.read`, `phone`)
162
+ - **Purpose**: Access protected resources and user data via your server.
163
+ - **Flow**: Requires a backend token exchange after authorization to retrieve data.
164
+ - **Mobile Scopes** (`mobile.geolocation`, `mobile.checkout`)
165
+ - **Purpose**: Access native device capabilities directly within the MiniApp.
166
+ - **Flow**: Grants in-app permission immediately; no backend exchange is necessary.
167
+
168
+ #### Authorization Patterns
169
+
170
+ When designing your MiniApp, you can choose between two common patterns for requesting scopes:
171
+
172
+ - **Upfront Authorization**
173
+ - Request all required scopes during app initialisation, typically alongside backend sign-in.
174
+ - _Best for_: Core permissions essential for the app to function.
175
+ - **Deferred Authorization**
176
+ - Request scopes only when the user triggers a specific feature that requires them.
177
+ - _Best for_: Optional permissions (e.g., location) to improve user experience and build trust.
178
+
179
+ #### Permission Verification Strategies
180
+
181
+ You can verify permissions either proactively before calling a method, or reactively by handling errors.
182
+
183
+ ##### Proactive Checking
184
+
185
+ Proactively verify if the current session has the necessary permissions for a method using `ScopeModule.hasAccessTo()`. This is recommended before calling gated methods, as users can revoke permissions at any time via the Grab app settings.
115
186
 
116
187
  ```typescript
117
188
  const scope = new ScopeModule();
@@ -123,6 +194,14 @@ if (isSuccess(hasAccess) && hasAccess.result) {
123
194
  }
124
195
  ```
125
196
 
197
+ ##### Reactive Checking (Handling 403 Forbidden)
198
+
199
+ Methods tagged with `@requiredOAuthScope` require specific permissions. If the user hasn't granted the required scope, the method returns `403`. You must request authorization and reload scopes before retrying:
200
+
201
+ 1. Call `IdentityModule.authorize()` to request the scope.
202
+ 2. Call `ScopeModule.reloadScopes()` to refresh the SDK's internal permission state.
203
+ 3. Retry the original method call.
204
+
126
205
  ```typescript
127
206
  import {
128
207
  LocationModule,
@@ -161,99 +240,71 @@ if (isError(response) && response.status_code === 403) {
161
240
  }
162
241
  ```
163
242
 
164
- ### Type Guards
165
-
166
- Type guards narrow the response type so TypeScript knows which fields are available:
167
-
168
- | Guard | Matches |
169
- | --------------------------------- | ---------------------------------------- |
170
- | `isSuccess(r)` | `200`, `204` |
171
- | `isOk(r)` | `200` |
172
- | `isNoContent(r)` | `204` |
173
- | `isRedirection(r)` / `isFound(r)` | `302` |
174
- | `isClientError(r)` | `400`, `401`, `403`, `404`, `424`, `426` |
175
- | `isServerError(r)` | `500`, `501` |
176
- | `isError(r)` | any 4xx or 5xx |
177
-
178
- ```typescript
179
- import { isSuccess, isOk, isNoContent, isError } from '@grabjs/superapp-sdk';
180
-
181
- if (isSuccess(response)) {
182
- // narrow further if needed
183
- if (isOk(response)) console.log(response.result);
184
- if (isNoContent(response)) console.log('done, no data');
185
- }
186
-
187
- if (isError(response)) {
188
- // response.error: string is guaranteed here
189
- console.error(response.error);
190
- }
191
- ```
192
-
193
- ### Streams
194
-
195
- Some modules provide streaming methods for real-time data (location updates, media events). Subscribe to receive values over time:
196
-
197
- ```typescript
198
- import { LocationModule, isSuccess } from '@grabjs/superapp-sdk';
199
-
200
- const location = new LocationModule();
201
-
202
- const subscription = location.observeLocationChange().subscribe({
203
- next: (response) => {
204
- if (isSuccess(response)) console.log(response.result);
205
- },
206
- complete: () => console.log('Stream ended'),
207
- });
208
-
209
- // Always unsubscribe when done to conserve battery and resources
210
- subscription.unsubscribe();
211
- ```
212
-
213
- You can also `await` a stream method directly to get its first value.
214
-
215
243
 
216
244
  ## Integration Guide
217
245
 
218
246
  This guide covers the recommended setup for a MiniApp entry point — loading scopes, configuring the container UI, signalling readiness, and handling permissions.
219
247
 
220
- ### Demo App
221
-
222
- The [demo](https://github.com/grab/superapp-sdk/tree/master/demo) folder contains two complete MiniApp samples demonstrating these integration patterns in action — one using CDN (vanilla HTML/JS) and one using React. Both implement the same user flow: OAuth authorization, user profile display, deferred location permissions, and checkout payment.
248
+ > **Note:** The [demo](https://github.com/grab/superapp-sdk/tree/master/demo) folder contains two complete MiniApp samples demonstrating these integration patterns in action — one using CDN (vanilla HTML/JS) and one using React. Both implement the same user flow: OAuth authorization, user profile display, deferred location permissions, and checkout payment.
223
249
 
224
- ### Entry Point Setup
250
+ ### Initialization
225
251
 
226
- Run these steps once when your MiniApp initialises, before rendering any content.
252
+ Follow these steps when your MiniApp launches to configure the container, authenticate the user, and track the entry event.
227
253
 
228
254
  ```typescript
229
- import { ContainerModule, ScopeModule } from '@grabjs/superapp-sdk';
255
+ import {
256
+ ContainerModule,
257
+ ScopeModule,
258
+ ContainerAnalyticsEventState,
259
+ isSuccess,
260
+ } from '@grabjs/superapp-sdk';
230
261
 
231
262
  const container = new ContainerModule();
232
263
  const scope = new ScopeModule();
233
264
 
234
265
  async function init() {
235
- // 1. Load permission scopes — always do this first
236
- await scope.reloadScopes();
266
+ // 1. Verify the environment
267
+ const connection = await container.isConnected();
268
+ if (!isSuccess(connection) || !connection.result?.connected) {
269
+ // Handle case where app is opened outside Grab SuperApp
270
+ return;
271
+ }
237
272
 
238
273
  // 2. Configure the container UI
239
274
  await container.setTitle('My MiniApp');
240
275
  await container.setBackgroundColor('#FFFFFF');
241
276
  await container.hideBackButton();
277
+ await container.hideRefreshButton();
242
278
 
243
279
  // 3. Dismiss the native loader
244
280
  await container.hideLoader();
281
+
282
+ // 4. Track app launch
283
+ await container.sendAnalyticsEvent({
284
+ state: ContainerAnalyticsEventState.HOMEPAGE,
285
+ name: 'DEFAULT',
286
+ });
287
+
288
+ // 5. Authenticate the user
289
+ // (Implementation detailed in the Authentication section below)
290
+ await signIn();
291
+
292
+ // 6. Load permission scopes — always do this before making module calls
293
+ await scope.reloadScopes();
245
294
  }
246
295
 
247
296
  init();
248
297
  ```
249
298
 
250
- #### Why `reloadScopes()` first?
299
+ ### Authentication
251
300
 
252
- Scopes are not loaded automatically. Any module call that requires a permission will return `403` until scopes are loaded. Always call `reloadScopes()` before making any other module calls.
301
+ Trigger `IdentityModule.authorize()` to start the authorization process and request user permissions.
253
302
 
254
- ### Handling Permissions
303
+ Once the user consents, retrieve the authorization artifacts (which include the `code`, `state`, `nonce`, and PKCE `codeVerifier`) via `IdentityModule.getAuthorizationArtifacts()`.
255
304
 
256
- When a module call returns `403`, your app needs to request the required permission via `IdentityModule.authorize()`, then reload scopes before retrying.
305
+ Forward these artifacts to your backend to exchange the token, validate the `id_token`, fetch user info, and establish the user's session.
306
+
307
+ After the session is established, call `IdentityModule.clearAuthorizationArtifacts()` and `ScopeModule.reloadScopes()` so your MiniApp can begin using the newly granted permissions.
257
308
 
258
309
  ```typescript
259
310
  import { IdentityModule, ScopeModule, isSuccess, isError } from '@grabjs/superapp-sdk';
@@ -261,45 +312,79 @@ import { IdentityModule, ScopeModule, isSuccess, isError } from '@grabjs/superap
261
312
  const identity = new IdentityModule();
262
313
  const scope = new ScopeModule();
263
314
 
264
- async function requestPermission() {
315
+ async function signIn() {
265
316
  const response = await identity.authorize({
266
317
  clientId: 'your-client-id',
267
318
  redirectUri: 'https://your-miniapp.example.com/callback',
268
- scope: 'required_scope',
319
+ scope: 'openid profile.read phone mobile.storage',
269
320
  environment: 'production',
321
+ responseMode: 'in_place',
270
322
  });
271
323
 
272
324
  if (isSuccess(response)) {
273
- // Reload scopes after authorization so the new permission is available
274
- await scope.reloadScopes();
325
+ if (response.status_code === 200) {
326
+ // 1. Retrieve authorization artifacts
327
+ const artifacts = await identity.getAuthorizationArtifacts();
328
+ if (isSuccess(artifacts)) {
329
+ const { codeVerifier, nonce, redirectUri } = artifacts.result;
330
+ const { code } = response.result;
331
+
332
+ // 2. Send the artifacts to your backend for token exchange (see Backend Token Exchange section below)
333
+ // await myBackend.exchangeTokens({ code, codeVerifier, nonce, redirectUri });
334
+
335
+ // 3. Clear artifacts and reload scopes
336
+ await identity.clearAuthorizationArtifacts();
337
+ await scope.reloadScopes();
338
+ }
339
+ } else if (response.status_code === 204) {
340
+ // User cancelled the authorization flow
341
+ await identity.clearAuthorizationArtifacts();
342
+ }
275
343
  } else if (isError(response)) {
276
344
  console.error('Authorization failed:', response.error);
345
+ await identity.clearAuthorizationArtifacts();
277
346
  }
278
347
  }
279
348
  ```
280
349
 
281
- ### Navigation
350
+ ### Container UI & Navigation
282
351
 
283
- #### Controlling the back button
352
+ Control the native container's appearance and behavior to match your MiniApp's branding and navigation flow.
284
353
 
285
- Hide the back button when your app manages its own navigation stack, and restore it when the user can safely go back:
354
+ #### Title and Background
355
+
356
+ Set the title and background color for the native container.
357
+
358
+ ```typescript
359
+ await container.setTitle('My MiniApp');
360
+ await container.setBackgroundColor('#FFFFFF');
361
+ ```
362
+
363
+ #### Back and Refresh Buttons
364
+
365
+ Hide these buttons when your MiniApp manages its own navigation or requires a focused, non-refreshable view. Restore them when appropriate.
286
366
 
287
367
  ```typescript
368
+ // Hide buttons
288
369
  await container.hideBackButton();
370
+ await container.hideRefreshButton();
289
371
 
290
- // ... when the user can go back
372
+ // Restore buttons
291
373
  await container.showBackButton();
374
+ await container.showRefreshButton();
292
375
  ```
293
376
 
294
377
  #### Closing the MiniApp
295
378
 
379
+ Programmatically close the MiniApp and return the user to the Grab SuperApp.
380
+
296
381
  ```typescript
297
382
  await container.close();
298
383
  ```
299
384
 
300
- #### Opening external links
385
+ ### Opening External Links
301
386
 
302
- Use `openExternalLink` to open URLs in the system browser instead of navigating away from the WebView:
387
+ Use `ContainerModule.openExternalLink()` to open URLs in the system browser instead of navigating away from the MiniApp WebView.
303
388
 
304
389
  ```typescript
305
390
  const response = await container.openExternalLink('https://example.com');
@@ -311,185 +396,47 @@ if (isError(response)) {
311
396
 
312
397
  ### Analytics Event Tracking
313
398
 
314
- Implement analytics events across your user journey to enable performance tracking and reporting. Events are sent via `ContainerModule.sendAnalyticsEvent()` and categorised by journey stage using `ContainerAnalyticsEventState`.
315
-
316
- #### Required Events
317
-
318
- ##### Entry Point
319
-
320
- ###### Initiate action
321
-
322
- Send the initiate event when users click call-to-action buttons to proceed:
399
+ Track user interactions to monitor performance and conversion. Events are categorised by journey stage using `ContainerAnalyticsEventState`.
323
400
 
324
401
  ```typescript
325
- import {
326
- ContainerModule,
327
- ContainerAnalyticsEventState,
328
- isSuccess,
329
- isError,
330
- } from '@grabjs/superapp-sdk';
331
-
332
- // Event names are plain strings — define your own constants
333
- const EventName = {
334
- INITIATE: 'INITIATE',
335
- TRANSACT: 'TRANSACT',
336
- };
402
+ import { ContainerModule, ContainerAnalyticsEventState, isSuccess } from '@grabjs/superapp-sdk';
337
403
 
338
- const containerModule = new ContainerModule();
404
+ const container = new ContainerModule();
339
405
 
340
- // Send when the user clicks a call-to-action button
341
- const response = await containerModule.sendAnalyticsEvent({
406
+ // 1. System Event (DEFAULT)
407
+ // Send when a user lands on a key page
408
+ await container.sendAnalyticsEvent({
342
409
  state: ContainerAnalyticsEventState.HOMEPAGE,
343
- name: EventName.INITIATE,
410
+ name: 'DEFAULT',
344
411
  });
345
412
 
346
- if (isSuccess(response)) {
347
- // Event sent successfully
348
- } else if (isError(response)) {
349
- console.error(`Failed to send event: ${response.error}`);
350
- }
351
- ```
352
-
353
- ###### Custom interactions
354
-
355
- Send custom events for additional interactions like banner clicks, category selections, or search queries. Include the `page` parameter and a descriptive event name:
356
-
357
- ```typescript
358
- import {
359
- ContainerModule,
360
- ContainerAnalyticsEventState,
361
- isSuccess,
362
- isError,
363
- } from '@grabjs/superapp-sdk';
364
-
365
- const containerModule = new ContainerModule();
413
+ // 2. Named Action (INITIATE / TRANSACT)
414
+ // Send when a user performs a primary action
415
+ await container.sendAnalyticsEvent({
416
+ state: ContainerAnalyticsEventState.HOMEPAGE,
417
+ name: 'INITIATE',
418
+ });
366
419
 
367
- // Send for custom interactions such as banner clicks or category selections
368
- const response = await containerModule.sendAnalyticsEvent({
420
+ // 3. Custom Interaction
421
+ // Send for specific interactions with additional metadata
422
+ await container.sendAnalyticsEvent({
369
423
  state: ContainerAnalyticsEventState.CUSTOM,
370
424
  name: 'BANNER_CLICK',
371
425
  data: {
372
426
  page: 'homepage',
373
427
  banner_id: 'promo-summer-2024',
374
- banner_position: 'top',
375
428
  },
376
429
  });
377
-
378
- if (isSuccess(response)) {
379
- // Event sent successfully
380
- } else if (isError(response)) {
381
- console.error(`Failed to send event: ${response.error}`);
382
- }
383
430
  ```
384
431
 
385
- ##### Conversion Point
432
+ #### Journey Stages
386
433
 
387
- ###### Transaction confirmation
388
-
389
- Send the transaction confirmation event when users click the final confirmation button:
390
-
391
- ```typescript
392
- import {
393
- ContainerModule,
394
- ContainerAnalyticsEventState,
395
- isSuccess,
396
- isError,
397
- } from '@grabjs/superapp-sdk';
398
-
399
- // Event names are plain strings — define your own constants
400
- const EventName = {
401
- INITIATE: 'INITIATE',
402
- TRANSACT: 'TRANSACT',
403
- };
404
-
405
- const containerModule = new ContainerModule();
406
-
407
- // Send when the user clicks the final confirmation button
408
- const response = await containerModule.sendAnalyticsEvent({
409
- state: ContainerAnalyticsEventState.CHECKOUT_PAGE,
410
- name: EventName.TRANSACT,
411
- data: {
412
- transaction_amount: 49.99,
413
- transaction_currency: 'SGD',
414
- transaction_id: 'TXN-2024-001234',
415
- payment_method: 'grabpay',
416
- item_count: 2,
417
- },
418
- });
419
-
420
- if (isSuccess(response)) {
421
- // Event sent successfully
422
- } else if (isError(response)) {
423
- console.error(`Failed to send event: ${response.error}`);
424
- }
425
- ```
426
-
427
- ###### Custom interactions
428
-
429
- Send custom events for checkout interactions like promo code applications or payment method selections:
430
-
431
- ```typescript
432
- import {
433
- ContainerModule,
434
- ContainerAnalyticsEventState,
435
- isSuccess,
436
- isError,
437
- } from '@grabjs/superapp-sdk';
438
-
439
- const containerModule = new ContainerModule();
440
-
441
- // Send for custom checkout interactions such as promo code applications
442
- const response = await containerModule.sendAnalyticsEvent({
443
- state: ContainerAnalyticsEventState.CUSTOM,
444
- name: 'PROMO_CODE_APPLIED',
445
- data: {
446
- page: 'checkout',
447
- promo_code: 'SAVE20',
448
- discount_amount: 10.0,
449
- discount_type: 'percentage',
450
- },
451
- });
452
-
453
- if (isSuccess(response)) {
454
- // Event sent successfully
455
- } else if (isError(response)) {
456
- console.error(`Failed to send event: ${response.error}`);
457
- }
458
- ```
459
-
460
- ##### Completion Point
461
-
462
- ###### Follow-up actions
463
-
464
- Send custom events for post-transaction actions like downloading receipts or tracking orders:
465
-
466
- ```typescript
467
- import {
468
- ContainerModule,
469
- ContainerAnalyticsEventState,
470
- isSuccess,
471
- isError,
472
- } from '@grabjs/superapp-sdk';
473
-
474
- const containerModule = new ContainerModule();
475
-
476
- // Send for post-transaction actions such as downloading a receipt
477
- const response = await containerModule.sendAnalyticsEvent({
478
- state: ContainerAnalyticsEventState.CUSTOM,
479
- name: 'RECEIPT_DOWNLOAD',
480
- data: {
481
- page: 'completion',
482
- transaction_id: 'TXN-2024-001234',
483
- format: 'pdf',
484
- },
485
- });
486
-
487
- if (isSuccess(response)) {
488
- // Event sent successfully
489
- } else if (isError(response)) {
490
- console.error(`Failed to send event: ${response.error}`);
491
- }
492
- ```
434
+ | State | Description |
435
+ | :----------------- | :----------------------------------------------- |
436
+ | `HOMEPAGE` | Entry point or main landing page. |
437
+ | `CHECKOUT_PAGE` | Transaction confirmation or payment selection. |
438
+ | `COMPLETION_POINT` | Post-transaction or success page. |
439
+ | `CUSTOM` | Any other interaction outside the standard flow. |
493
440
 
494
441
  #### Best Practices
495
442
 
@@ -502,16 +449,6 @@ if (isSuccess(response)) {
502
449
 
503
450
  The checkout flow is a two-step process: your backend first initializes a transaction using your partner credentials, then your frontend triggers the native payment interface using the response from your backend.
504
451
 
505
- #### Step 1 — Initialize transaction on your backend
506
-
507
- Before calling `CheckoutModule.triggerCheckout()`, you must create a transaction on your server by calling the [GrabPay API](https://developer.grab.com/docs/partner-apps/pages/developer-resources/payment/#create-transaction). This requires your `partnerID` and `partnerSecret` to generate an HMAC-SHA256 signature.
508
-
509
- The API returns a payload containing `partnerTxID`, `request`, and `sessionID` — all three fields are required by the frontend.
510
-
511
- #### Step 2 — Trigger checkout from your frontend
512
-
513
- Pass the complete response from the Initialize Transaction API to `CheckoutModule.triggerCheckout()`:
514
-
515
452
  ```typescript
516
453
  import {
517
454
  CheckoutModule,
@@ -526,20 +463,25 @@ const identity = new IdentityModule();
526
463
  const scope = new ScopeModule();
527
464
 
528
465
  async function processPayment() {
529
- // 1. Ensure mobile.checkout scope is authorized
530
- const authResponse = await identity.authorize({
531
- clientId: 'your-client-id',
532
- redirectUri: 'https://your-miniapp.example.com/callback',
533
- scope: 'mobile.checkout',
534
- environment: 'production',
535
- responseMode: 'in_place',
536
- });
537
-
538
- if (isSuccess(authResponse)) {
539
- await scope.reloadScopes();
540
- } else if (isError(authResponse)) {
541
- console.error('Authorization failed:', authResponse.error);
542
- return;
466
+ // 1. Proactively check for checkout permission
467
+ const hasAccess = await scope.hasAccessTo('CheckoutModule', 'triggerCheckout');
468
+
469
+ if (!isSuccess(hasAccess) || !hasAccess.result) {
470
+ // Request authorization for mobile.checkout
471
+ // Note: mobile.checkout is a mobile scope; no backend exchange is needed for auth.
472
+ const authResponse = await identity.authorize({
473
+ clientId: 'your-client-id',
474
+ redirectUri: window.location.href,
475
+ scope: 'mobile.checkout',
476
+ environment: 'production',
477
+ responseMode: 'in_place',
478
+ });
479
+
480
+ if (isSuccess(authResponse) && authResponse.status_code === 200) {
481
+ await scope.reloadScopes();
482
+ } else {
483
+ return;
484
+ }
543
485
  }
544
486
 
545
487
  // 2. Fetch the initialized transaction payload from your backend
@@ -558,33 +500,14 @@ async function processPayment() {
558
500
  });
559
501
 
560
502
  if (isSuccess(checkoutResult)) {
561
- const { status, transactionID, errorCode, errorMessage } = checkoutResult.result;
562
-
563
- if (status === 'success') {
564
- console.log('Payment successful:', transactionID);
565
- } else if (status === 'failure') {
566
- console.error('Payment failed:', errorCode, errorMessage);
567
- } else if (status === 'pending') {
568
- console.log('Payment is processing:', transactionID);
569
- } else if (status === 'userInitiatedCancel') {
570
- console.log('User cancelled payment');
571
- }
503
+ console.log(checkoutResult.result);
572
504
  } else if (isError(checkoutResult)) {
573
505
  console.error('Checkout error:', checkoutResult.error);
574
506
  }
575
507
  }
576
508
  ```
577
509
 
578
- #### Result status values
579
-
580
- | Status | Description |
581
- | :-------------------- | :----------------------------------------------------------------------------- |
582
- | `success` | Payment completed successfully. `transactionID` is provided. |
583
- | `failure` | Payment failed. `transactionID`, `errorCode`, and `errorMessage` are provided. |
584
- | `pending` | Payment is still being processed. `transactionID` is provided. |
585
- | `userInitiatedCancel` | User cancelled the payment. No other fields are present. |
586
-
587
- For the complete API reference, see [CheckoutModule](https://grab.github.io/superapp-sdk/classes/CheckoutModule.html).
510
+ For the complete API reference, see [GrabPay API](https://developer.grab.com/docs/partner-apps/pages/developer-resources/payment/) and [CheckoutModule](https://grab.github.io/superapp-sdk/classes/CheckoutModule.html).
588
511
 
589
512
 
590
513
  ## API Reference
@@ -679,10 +602,10 @@ JSBridge module for controlling the native splash / Lottie loading screen.
679
602
 
680
603
  #### `StorageModule`
681
604
  JSBridge module for persisting key-value data to native storage.
682
- - `getBoolean(key: string): Promise<{ error: string; status_code: 400 } | { error: string; status_code: 500 } | { error: string; status_code: 501 } | { error: string; status_code: 424 } | { result: { value: boolean | null }; status_code: 200 }>` — Retrieves a boolean value from the native storage. (**OAuth Scope:** mobile.storage)
683
- - `getDouble(key: string): Promise<{ error: string; status_code: 400 } | { error: string; status_code: 500 } | { error: string; status_code: 501 } | { error: string; status_code: 424 } | { result: { value: number | null }; status_code: 200 }>` — Retrieves a double (floating point) value from the native storage. (**OAuth Scope:** mobile.storage)
684
- - `getInt(key: string): Promise<{ error: string; status_code: 400 } | { error: string; status_code: 500 } | { error: string; status_code: 501 } | { error: string; status_code: 424 } | { result: { value: number | null }; status_code: 200 }>` — Retrieves an integer value from the native storage. (**OAuth Scope:** mobile.storage)
685
- - `getString(key: string): Promise<{ error: string; status_code: 400 } | { error: string; status_code: 500 } | { error: string; status_code: 501 } | { error: string; status_code: 424 } | { result: { value: string | null }; status_code: 200 }>` — Retrieves a string value from the native storage. (**OAuth Scope:** mobile.storage)
605
+ - `getBoolean(key: string): Promise<{ status_code: 204 } | { error: string; status_code: 400 } | { error: string; status_code: 500 } | { error: string; status_code: 501 } | { result: boolean; status_code: 200 } | { error: string; status_code: 424 }>` — Retrieves a boolean value from the native storage. (**OAuth Scope:** mobile.storage)
606
+ - `getDouble(key: string): Promise<{ status_code: 204 } | { error: string; status_code: 400 } | { error: string; status_code: 500 } | { error: string; status_code: 501 } | { error: string; status_code: 424 } | { result: number; status_code: 200 }>` — Retrieves a double (floating point) value from the native storage. (**OAuth Scope:** mobile.storage)
607
+ - `getInt(key: string): Promise<{ status_code: 204 } | { error: string; status_code: 400 } | { error: string; status_code: 500 } | { error: string; status_code: 501 } | { error: string; status_code: 424 } | { result: number; status_code: 200 }>` — Retrieves an integer value from the native storage. (**OAuth Scope:** mobile.storage)
608
+ - `getString(key: string): Promise<{ status_code: 204 } | { error: string; status_code: 400 } | { error: string; status_code: 500 } | { error: string; status_code: 501 } | { result: string; status_code: 200 } | { error: string; status_code: 424 }>` — Retrieves a string value from the native storage. (**OAuth Scope:** mobile.storage)
686
609
  - `remove(key: string): Promise<{ status_code: 204 } | { error: string; status_code: 400 } | { error: string; status_code: 500 } | { error: string; status_code: 501 } | { error: string; status_code: 424 }>` — Removes a single value from the native storage by key. (**OAuth Scope:** mobile.storage)
687
610
  - `removeAll(): Promise<{ status_code: 204 } | { error: string; status_code: 500 } | { error: string; status_code: 501 } | { error: string; status_code: 424 }>` — Removes all values from the native storage. (**OAuth Scope:** mobile.storage)
688
611
  - `setBoolean(key: string, value: boolean): Promise<{ status_code: 204 } | { error: string; status_code: 400 } | { error: string; status_code: 500 } | { error: string; status_code: 501 } | { error: string; status_code: 424 }>` — Stores a boolean value in the native storage. (**OAuth Scope:** mobile.storage)