Skip to content
Merged
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
39 changes: 35 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ experiences.
- [Checkout lifecycle](#checkout-lifecycle)
- [`addEventListener(eventName, callback)`](#addeventlistenereventname-callback)
- [`removeEventListeners(eventName)`](#removeeventlistenerseventname)
- [Error handling](#error-handling)
- [Error types](#error-types)
- [Error codes](#error-codes)
- [Identity \& customer accounts](#identity--customer-accounts)
- [Cart: buyer bag, identity, and preferences](#cart-buyer-bag-identity-and-preferences)
- [Multipass](#multipass)
Expand Down Expand Up @@ -581,7 +584,7 @@ methods - available on both the context provider as well as the class instance.
| `close` | `() => void` | Fired when the checkout has been closed. |
| `complete` | `(event: CheckoutCompleteEvent) => void` | Fired when the checkout has been successfully completed. |
| `start` | `(event: CheckoutStartEvent) => void` | Fired when the checkout has been started. |
| `error` | `(error: {message: string}) => void` | Fired when a checkout exception has been raised. |
| `error` | `(error: CheckoutException) => void` | Fired when a checkout exception has been raised. See [Error handling](#error-handling) below. |

### `addEventListener(eventName, callback)`

Expand All @@ -608,9 +611,9 @@ useEffect(() => {

const error = shopifyCheckout.addEventListener(
'error',
(error: CheckoutError) => {
// Do something on checkout error
// console.log(error.message)
(error: CheckoutException) => {
// Handle checkout error - see "Error handling" section for details
console.log(error.message, error.code, error.recoverable);
},
);

Expand All @@ -628,6 +631,34 @@ useEffect(() => {
On the rare occasion that you want to remove all event listeners for a given
`eventName`, you can use the `removeEventListeners(eventName)` method.

### Error handling

The `error` event provides a `CheckoutException` object with detailed information about what went wrong. Each error includes:

| Property | Type | Description |
| ------------- | ------------------- | -------------------------------------------------------------- |
| `message` | `string` | A human-readable error message. |
| `code` | `CheckoutErrorCode` | A machine-readable error code (see table below). |
| `recoverable` | `boolean` | Whether the error is recoverable (e.g., retry may succeed). |
| `name` | `string` | The error class name (e.g., `ConfigurationError`). |
| `statusCode` | `number` (optional) | HTTP status code (only present for `CheckoutHTTPError`). |

#### Error types

Errors are returned as instances of specific error classes:

| Error Class | Description |
| --------------------- | --------------------------------------------------------------------------- |
| `ConfigurationError` | The checkout configuration is invalid (e.g., invalid credentials). |
| `CheckoutClientError` | A client-side error occurred (e.g., checkout unavailable). |
| `CheckoutExpiredError`| The checkout session has expired or the cart is no longer valid. |
| `CheckoutHTTPError` | An HTTP error occurred. Includes `statusCode` property. |
| `InternalError` | An internal SDK error occurred. |

#### Error codes

The `code` property uses the `CheckoutErrorCode` enum. See [`errors.d.ts`](./modules/@shopify/checkout-sheet-kit/src/errors.d.ts) for the full list of error codes and their descriptions.

## Identity & customer accounts

Buyer-aware checkout experience reduces friction and increases conversion.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ ndkVersion=23.1.7779620
buildToolsVersion = "35.0.0"

# Version of Shopify Checkout SDK to use with React Native
SHOPIFY_CHECKOUT_SDK_VERSION=4.0.0-SNAPSHOT
SHOPIFY_CHECKOUT_SDK_VERSION=4.0.1-SNAPSHOT
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,6 @@ of this software and associated documentation files (the "Software"), to deal
import java.util.Map;
import java.util.Objects;

import kotlin.Unit;

public class RCTCheckoutWebView extends FrameLayout {
private static final String TAG = "RCTCheckoutWebView";
private final ThemedReactContext context;
Expand Down Expand Up @@ -195,13 +193,7 @@ private CheckoutWebViewEventProcessor getCheckoutWebViewEventProcessor() {
Activity currentActivity = this.context.getCurrentActivity();
InlineCheckoutEventProcessor eventProcessor = new InlineCheckoutEventProcessor(currentActivity);

return new CheckoutWebViewEventProcessor(
eventProcessor,
(visible) -> Unit.INSTANCE, // toggleHeader
(error) -> Unit.INSTANCE, // closeCheckoutDialogWithError
(visibility) -> Unit.INSTANCE, // setProgressBarVisibility
(percentage) -> Unit.INSTANCE // updateProgressBarPercentage
);
return new CheckoutWebViewEventProcessor(eventProcessor);
}

void removeCheckout() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,15 @@ internal enum ShopifyEventSerialization {
*/
static func serialize(checkoutError error: CheckoutError) -> [String: Any] {
switch error {
case let .checkoutExpired(message, code, recoverable):
case let .expired(message, code, recoverable):
return [
"__typename": "CheckoutExpiredError",
"message": message,
"code": code.rawValue,
"recoverable": recoverable
]

case let .checkoutUnavailable(message, code, recoverable):
case let .unavailable(message, code, recoverable):
switch code {
case let .clientError(clientErrorCode):
return [
Expand All @@ -139,15 +139,15 @@ internal enum ShopifyEventSerialization {
]
}

case let .configurationError(message, code, recoverable):
case let .misconfiguration(message, code, recoverable):
return [
"__typename": "ConfigurationError",
"message": message,
"code": code.rawValue,
"recoverable": recoverable
]

case let .sdkError(underlying, recoverable):
case let .internal(underlying, recoverable):
return [
"__typename": "InternalError",
"code": "unknown",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class RCTShopifyCheckoutSheetKit: RCTEventEmitter, CheckoutDelegate {
}

override func supportedEvents() -> [String]! {
return ["close", "complete", "start", "error", "addressChangeStart", "submitStart"]
return ["close", "complete", "start", "error", "addressChangeStart", "submitStart", "paymentMethodChangeStart"]
}

override func startObserving() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ import type {
CheckoutStartEvent,
CheckoutSubmitStartEvent,
} from '../events';
import type {CheckoutException} from '../errors';
import {
parseCheckoutError,
type CheckoutException,
type CheckoutNativeError,
} from '../errors.d';

export interface ShopifyCheckoutProps {
/**
Expand Down Expand Up @@ -125,7 +129,7 @@ interface NativeShopifyCheckoutWebViewProps {
style?: ViewStyle;
testID?: string;
onStart?: (event: {nativeEvent: CheckoutStartEvent}) => void;
onError?: (event: {nativeEvent: CheckoutException}) => void;
onError?: (event: {nativeEvent: CheckoutNativeError}) => void;
onComplete?: (event: {nativeEvent: CheckoutCompleteEvent}) => void;
onCancel?: () => void;
onLinkClick?: (event: {nativeEvent: {url: string}}) => void;
Expand Down Expand Up @@ -231,7 +235,8 @@ export const ShopifyCheckout = forwardRef<
Required<NativeShopifyCheckoutWebViewProps>['onError']
>(
event => {
onError?.(event.nativeEvent);
const transformedError = parseCheckoutError(event.nativeEvent);
onError?.(transformedError);
},
[onError],
);
Expand Down
149 changes: 145 additions & 4 deletions modules/@shopify/checkout-sheet-kit/src/errors.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,118 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

/**
* Error codes that can be returned from checkout errors.
*/
export enum CheckoutErrorCode {
// ============================================================================
// Configuration errors
// ============================================================================

/**
* The app authentication payload passed could not be decoded.
*/
invalidPayload = 'invalid_payload',

/**
* The app authentication JWT signature or encrypted token signature was invalid.
*/
invalidSignature = 'invalid_signature',

/**
* The app authentication access token was not valid for the shop.
*/
notAuthorized = 'not_authorized',

/**
* The provided app authentication payload has expired.
*/
payloadExpired = 'payload_expired',

/**
* The buyer must be logged in to a customer account to proceed with checkout.
*/
customerAccountRequired = 'customer_account_required',

/**
* The storefront requires a password to access checkout.
*/
storefrontPasswordRequired = 'storefront_password_required',
cartExpired = 'cart_expired',

// ============================================================================
// Cart errors
// ============================================================================

/**
* The cart associated with the checkout has already been completed.
*/
cartCompleted = 'cart_completed',

/**
* The cart is invalid or no longer exists.
*/
invalidCart = 'invalid_cart',

// ============================================================================
// Client errors
// ============================================================================

/**
* Checkout preloading has been temporarily disabled via killswitch.
*/
killswitchEnabled = 'killswitch_enabled',

/**
* An unrecoverable error occurred during checkout.
*/
unrecoverableFailure = 'unrecoverable_failure',

/**
* A policy violation was detected during checkout.
*/
policyViolation = 'policy_violation',

/**
* An error occurred processing a vaulted payment method.
*/
vaultedPaymentError = 'vaulted_payment_error',

// ============================================================================
// Internal errors
// ============================================================================

/**
* A client-side error occurred in the SDK.
*/
clientError = 'client_error',

/**
* An HTTP error occurred while communicating with the checkout.
*/
httpError = 'http_error',

/**
* Failed to send a message to the checkout bridge.
*/
sendingBridgeEventError = 'error_sending_message',

/**
* Failed to receive a message from the checkout bridge.
*/
receivingBridgeEventError = 'error_receiving_message',

/**
* The WebView render process has terminated unexpectedly (Android only).
*/
renderProcessGone = 'render_process_gone',

// ============================================================================
// Fallback
// ============================================================================

/**
* An unknown or unrecognized error code was received.
*/
unknown = 'unknown',
}

Expand All @@ -43,17 +145,28 @@ export enum CheckoutNativeErrorType {
UnknownError = 'UnknownError',
}

/**
* Maps a native error code string to a CheckoutErrorCode enum value.
*/
function getCheckoutErrorCode(code: string | undefined): CheckoutErrorCode {
if (!code) {
return CheckoutErrorCode.unknown;
}

const normalizedCode = code.toLowerCase();

const codeKey = Object.keys(CheckoutErrorCode).find(
key => CheckoutErrorCode[key as keyof typeof CheckoutErrorCode] === code,
key => CheckoutErrorCode[key as keyof typeof CheckoutErrorCode] === normalizedCode,
);

return codeKey ? CheckoutErrorCode[codeKey] : CheckoutErrorCode.unknown;
return codeKey
? CheckoutErrorCode[codeKey as keyof typeof CheckoutErrorCode]
: CheckoutErrorCode.unknown;
}

type BridgeError = {
__typename: CheckoutNativeErrorType;
code: CheckoutErrorCode;
code: string;
message: string;
recoverable: boolean;
};
Expand Down Expand Up @@ -115,11 +228,13 @@ export class InternalError {
code: CheckoutErrorCode;
message: string;
recoverable: boolean;
name: string;

constructor(exception: CheckoutNativeError) {
this.code = getCheckoutErrorCode(exception.code);
this.message = exception.message;
this.recoverable = exception.recoverable;
this.name = this.constructor.name;
}
}

Expand All @@ -130,3 +245,29 @@ export type CheckoutException =
| ConfigurationError
| GenericError
| InternalError;

/**
* Transforms a native error object into the appropriate CheckoutException class.
* Maps __typename to the correct error class and normalizes error codes.
*
* @param exception Raw error object from native bridge
* @returns Appropriate CheckoutException instance
*/
export function parseCheckoutError(
exception: CheckoutNativeError,
): CheckoutException {
switch (exception?.__typename) {
case CheckoutNativeErrorType.InternalError:
return new InternalError(exception);
case CheckoutNativeErrorType.ConfigurationError:
return new ConfigurationError(exception);
case CheckoutNativeErrorType.CheckoutClientError:
return new CheckoutClientError(exception);
case CheckoutNativeErrorType.CheckoutHTTPError:
return new CheckoutHTTPError(exception);
case CheckoutNativeErrorType.CheckoutExpiredError:
return new CheckoutExpiredError(exception);
default:
return new GenericError(exception);
}
}
Loading