Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ of this software and associated documentation files (the "Software"), to deal
package com.shopify.reactnative.checkoutsheetkit;

import android.content.Context;
import android.net.Uri;
import android.util.Log;
import android.webkit.GeolocationPermissions;

Expand Down Expand Up @@ -212,6 +213,17 @@ public void onSubmitStart(@NonNull CheckoutSubmitStartEvent event) {
}
}

@Override
public void onLinkClick(@NonNull Uri uri) {
try {
Map<String, Object> event = new HashMap<>();
event.put("url", uri.toString());
sendEventWithStringData("linkClick", mapper.writeValueAsString(event));
} catch (IOException e) {
Log.e(TAG, "Error processing link click event", e);
}
}

// Private
private Map<String, Object> populateErrorDetails(CheckoutException error) {
Map<String, Object> errorMap = new HashMap<>(Map.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ of this software and associated documentation files (the "Software"), to deal
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;

import android.content.Context;
import android.net.Uri;
import android.util.Log;
import android.webkit.GeolocationPermissions;

Expand Down Expand Up @@ -439,6 +440,35 @@ public void testOnSubmitStart_withSerializationError_logsErrorAndDoesNotEmit() t
mockedLog.verify(() -> Log.e(eq("SheetCheckoutEventProcessor"), eq("Error processing submit start event"), any(IOException.class)));
}

// MARK: - onLinkClick Tests

@Test
public void testOnLinkClick_emitsLinkClickEventWithUrl() {
Uri mockUri = mock(Uri.class);
when(mockUri.toString()).thenReturn("https://example.com/terms");

processor.onLinkClick(mockUri);

verify(mockEventEmitter).emit(eventNameCaptor.capture(), eventDataCaptor.capture());
assertThat(eventNameCaptor.getValue()).isEqualTo("linkClick");
String eventData = eventDataCaptor.getValue();
assertThat(eventData).contains("\"url\":\"https://example.com/terms\"");
}

@Test
public void testOnLinkClick_withSerializationError_logsErrorAndDoesNotEmit() throws Exception {
ObjectMapper mockMapper = mock(ObjectMapper.class);
when(mockMapper.writeValueAsString(any())).thenThrow(new JsonProcessingException("Serialization failed") {});
setPrivateField(processor, "mapper", mockMapper);

Uri mockUri = mock(Uri.class);
when(mockUri.toString()).thenReturn("https://example.com/terms");
processor.onLinkClick(mockUri);

verify(mockEventEmitter, never()).emit(anyString(), anyString());
mockedLog.verify(() -> Log.e(eq("SheetCheckoutEventProcessor"), eq("Error processing link click event"), any(IOException.class)));
}

// MARK: - Helper Methods

private void setPrivateField(Object object, String fieldName, Object value) throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class RCTShopifyCheckoutSheetKit: RCTEventEmitter, CheckoutDelegate {
case paymentMethodChangeStart
case start
case submitStart
case linkClick
}

override init() {
Expand Down Expand Up @@ -110,6 +111,10 @@ class RCTShopifyCheckoutSheetKit: RCTEventEmitter, CheckoutDelegate {
}
}

func checkoutDidClickLink(url: URL) {
emit(event: .linkClick, body: ["url": url.absoluteString])
}

@objc override func constantsToExport() -> [AnyHashable: Any]! {
return [
"version": ShopifyCheckoutSheetKit.version
Expand Down
16 changes: 14 additions & 2 deletions modules/@shopify/checkout-sheet-kit/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,16 +154,22 @@ export type CheckoutEvent =
| 'error'
| 'addressChangeStart'
| 'submitStart'
| 'geolocationRequest';
| 'geolocationRequest'
| 'linkClick';

export interface GeolocationRequestEvent {
origin: string;
}

export interface LinkClickEvent {
url: string;
}

export type CloseEventCallback = () => void;
export type GeolocationRequestEventCallback = (
event: GeolocationRequestEvent,
) => void;
export type LinkClickEventCallback = (event: LinkClickEvent) => void;
export type CheckoutExceptionCallback = (error: CheckoutException) => void;
export type CheckoutCompleteEventCallback = (
event: CheckoutCompleteEvent,
Expand All @@ -185,7 +191,8 @@ export type CheckoutEventCallback =
| CheckoutStartEventCallback
| CheckoutAddressChangeStartCallback
| CheckoutSubmitStartCallback
| GeolocationRequestEventCallback;
| GeolocationRequestEventCallback
| LinkClickEventCallback;

/**
* Available wallet types for accelerated checkout
Expand Down Expand Up @@ -283,6 +290,11 @@ function addEventListener(
callback: GeolocationRequestEventCallback,
): Maybe<EmitterSubscription>;

function addEventListener(
event: 'linkClick',
callback: LinkClickEventCallback,
): Maybe<EmitterSubscription>;

function removeEventListeners(event: CheckoutEvent): void;

export type AddEventListener = typeof addEventListener;
Expand Down
5 changes: 5 additions & 0 deletions modules/@shopify/checkout-sheet-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type {
Configuration,
Features,
GeolocationRequestEvent,
LinkClickEvent,
Maybe,
ShopifyCheckoutSheetKit,
} from './index.d';
Expand Down Expand Up @@ -208,6 +209,9 @@ class ShopifyCheckoutSheet implements ShopifyCheckoutSheetKit {
callback,
);
break;
case 'linkClick':
eventCallback = this.interceptEventEmission('linkClick', callback);
break;
default:
eventCallback = callback;
}
Expand Down Expand Up @@ -475,6 +479,7 @@ export type {
Configuration,
Features,
GeolocationRequestEvent,
LinkClickEvent,
};

// Event types
Expand Down
36 changes: 36 additions & 0 deletions modules/@shopify/checkout-sheet-kit/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,42 @@ describe('ShopifyCheckoutSheetKit', () => {
});
});

describe('LinkClick Event', () => {
it('parses linkClick event string data as JSON', () => {
const instance = new ShopifyCheckoutSheet();
const callback = jest.fn();
instance.addEventListener('linkClick', callback);

eventEmitter.emit(
'linkClick',
JSON.stringify({url: 'https://example.com/terms'}),
);
expect(callback).toHaveBeenCalledWith({url: 'https://example.com/terms'});
});

it('parses linkClick event JSON data', () => {
const instance = new ShopifyCheckoutSheet();
const callback = jest.fn();
instance.addEventListener('linkClick', callback);

eventEmitter.emit('linkClick', {url: 'https://example.com/privacy'});
expect(callback).toHaveBeenCalledWith({url: 'https://example.com/privacy'});
});

it('prints an error if the linkClick event data cannot be parsed', () => {
const mock = jest.spyOn(global.console, 'error');
const instance = new ShopifyCheckoutSheet();
const callback = jest.fn();
instance.addEventListener('linkClick', callback);

eventEmitter.emit('linkClick', 'INVALID JSON');
expect(mock).toHaveBeenCalledWith(
expect.any(LifecycleEventParseError),
'INVALID JSON',
);
});
});

describe('Error Event', () => {
const internalError = {
__typename: CheckoutNativeErrorType.InternalError,
Expand Down
14 changes: 14 additions & 0 deletions sample/ios/ReactNativeTests/ShopifyCheckoutSheetKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,20 @@ class ShopifyCheckoutSheetKitTests: XCTestCase {
XCTAssertTrue((mock.checkoutSheet as! MockCheckoutSheet).dismissWasCalled)
}

/// checkoutDidClickLink
func testCheckoutDidClickLinkSendsEvent() {
let mock = mockSendEvent(eventName: "linkClick")

mock.startObserving()

let testURL = URL(string: "https://example.com/terms")!
mock.checkoutDidClickLink(url: testURL)

XCTAssertTrue(mock.didSendEvent)
let body = mock.eventBody as? [String: String]
XCTAssertEqual(body?["url"], "https://example.com/terms")
}

/// CheckoutOptions parsing
func testCanPresentCheckoutWithAuthenticationOptions() {
let options: [AnyHashable: Any] = [
Expand Down
5 changes: 5 additions & 0 deletions sample/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,17 @@ function AppWithContext({children}: PropsWithChildren) {
},
);

const linkClick = shopify.addEventListener('linkClick', event => {
console.log('[App] linkClick event received:', event.url);
});

return () => {
completed?.remove();
started?.remove();
addressChangeStart?.remove();
close?.remove();
error?.remove();
linkClick?.remove();
};
}, [shopify, eventHandlers]);

Expand Down
Loading