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
15 changes: 13 additions & 2 deletions modules/@shopify/checkout-sheet-kit/ios/RCTCheckoutWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ class RCTCheckoutWebView: UIView {

private var events: EventBus = .init()

private var parentViewController: UIViewController? {
var responder: UIResponder? = self
while let nextResponder = responder?.next {
if let viewController = nextResponder as? UIViewController {
return viewController
}
responder = nextResponder
}
return nil
}

/// Public Properties
@objc var checkoutUrl: String?
@objc var checkoutOptions: [AnyHashable: Any]?
Expand Down Expand Up @@ -255,15 +266,15 @@ extension RCTCheckoutWebView: CheckoutDelegate {
error.isRecoverable
}

func checkoutDidRequestAddressChange(event: AddressChangeRequest) {
func checkoutDidRequestAddressChange(event: AddressChangeRequested) {
guard let id = event.id else { return }

self.events.set(key: id, event: event)

onAddressChangeIntent?([
"id": event.id,
"type": "addressChangeIntent",
"addressType": event.addressType,
"addressType": event.params.addressType,
])
}
}
4 changes: 4 additions & 0 deletions sample/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ LAST_NAME="Hartley"
PROVINCE="ON"
ZIP="M5V 1M7"
PHONE="1-888-746-7439"

APP_API_KEY=<your apps api key>
APP_SHARED_SECRET=<your apps shared secret>
APP_ACCESS_TOKEN=<your apps access token>
2 changes: 1 addition & 1 deletion sample/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2891,6 +2891,6 @@ SPEC CHECKSUMS:
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: a742cc68e8366fcfc681808162492bc0aa7a9498

PODFILE CHECKSUM: 178001eab8e8a8100c91d0b91866b5a76e0291fb
PODFILE CHECKSUM: d64592b0776174ac530c063d582a7b9439ff7a5a

COCOAPODS: 1.15.2
4 changes: 4 additions & 0 deletions sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"@react-navigation/bottom-tabs": "^7.4.6",
"@react-navigation/stack": "^7.4.8",
"@shopify/checkout-sheet-kit": "link:../modules/@shopify/checkout-sheet-kit",
"buffer": "^6.0.3",
"crypto-js": "^4.2.0",
"graphql": "^16.8.2",
"jotai": "^2.13.1",
"react-native-config": "1.5.6",
Expand Down Expand Up @@ -49,6 +51,8 @@
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.27.6",
"@react-native-masked-view/masked-view": "^0.3.2",
"@types/crypto-js": "^4.2.2",
"@types/node": "^24.9.1",
"@types/react-native-vector-icons": "^6.4.18",
"@types/setimmediate": "^1",
"babel-plugin-module-resolver": "^5.0.0",
Expand Down
85 changes: 85 additions & 0 deletions sample/src/config/authConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
MIT License

Copyright 2023 - Present, Shopify Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
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.
*/

import Config from 'react-native-config';

const {
APP_API_KEY,
APP_SHARED_SECRET,
APP_ACCESS_TOKEN,
} = Config;

/**
* ⚠️ WARNING: FOR TESTING ONLY ⚠️
*
* This configuration is for local testing of authentication flows.
* DO NOT USE IN PRODUCTION. JWT tokens must be generated server-side.
*
* To enable authentication testing:
* 1. Add your test app credentials to .env file:
* APP_API_KEY=your-api-key
* APP_SHARED_SECRET=your-shared-secret
* APP_ACCESS_TOKEN=your-access-token
* 2. Run the sample app
* 3. Go to Settings and toggle "App authentication" ON
*
* These values should match what you configure in your Shopify app settings.
*/

export interface AuthConfig {
/**
* Your app's API key
* Found in your Shopify Partner dashboard under app settings
*/
apiKey: string;

/**
* Your app's shared secret
* Found in your Shopify Partner dashboard under app settings
*/
sharedSecret: string;

/**
* Your app's access token
* This would typically be obtained during app installation
*/
accessToken: string;
}

export const authConfig: AuthConfig = {
apiKey: APP_API_KEY || '',
sharedSecret: APP_SHARED_SECRET || '',
accessToken: APP_ACCESS_TOKEN || '',
};

/**
* Validates that all required auth configuration is present
*/
export function hasAuthCredentials(): boolean {
return !!(
authConfig.apiKey &&
authConfig.sharedSecret &&
authConfig.accessToken
);
}

2 changes: 2 additions & 0 deletions sample/src/context/Config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface AppConfig {
enablePreloading: boolean;
prefillBuyerInformation: boolean;
customerAuthenticated: boolean;
appAuthenticationEnabled: boolean;
}

interface Context {
Expand All @@ -26,6 +27,7 @@ const defaultAppConfig: AppConfig = {
enablePreloading: true,
prefillBuyerInformation: true,
customerAuthenticated: false,
appAuthenticationEnabled: false,
};

const ConfigContext = createContext<Context>({
Expand Down
66 changes: 46 additions & 20 deletions sample/src/screens/BuyNow/CheckoutScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,49 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO

import type {NavigationProp, RouteProp} from '@react-navigation/native';
import {useNavigation} from '@react-navigation/native';
import React, {useRef} from 'react';
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {
Checkout,
type CheckoutRef,
type CheckoutOptions,
} from '@shopify/checkout-sheet-kit';
import type {BuyNowStackParamList} from './types';
import {StyleSheet} from 'react-native';
import {authConfig, hasAuthCredentials} from '../../config/authConfig';
import {generateAuthToken} from '../../utils/crypto/jwtTokenGenerator';
import {useConfig} from '../../context/Config';

/**
* Hook that fetches an authentication token from the authorization server.
* Hook that generates/fetches an authentication token.
*
* For testing: Uses client-side JWT generation (NOT production safe)
* For production: Replace with API call to your authorization server
*
* @param enabled - Whether authentication is enabled (from Settings toggle)
*/
function useAuth(): string | undefined {
// Example:
// const [token, setToken] = useState<string | undefined>();
// useEffect(() => {
// fetchTokenFromServer().then(setToken);
// }, []);
// return token;

return undefined;
function useAuth(enabled: boolean): string | undefined {
const [token, setToken] = useState<string | undefined>();

useEffect(() => {
if (!enabled || !hasAuthCredentials()) {
setToken(undefined);
return;
}

try {
const generatedToken = generateAuthToken(
authConfig.apiKey,
authConfig.sharedSecret,
authConfig.accessToken,
);
setToken(generatedToken ?? undefined);
} catch (error) {
console.error('[CheckoutScreen] Auth token generation error:', error);
setToken(undefined);
}
}, [enabled]);

return token;
}

// This component represents a screen in the consumers app that
Expand All @@ -53,15 +75,19 @@ export default function CheckoutScreen(props: {
}) {
const navigation = useNavigation<NavigationProp<BuyNowStackParamList>>();
const ref = useRef<CheckoutRef>(null);
const authToken = useAuth();

const checkoutOptions: CheckoutOptions | undefined = authToken
? {
authentication: {
token: authToken,
},
}
: undefined;
const {appConfig} = useConfig();
const authToken = useAuth(appConfig.appAuthenticationEnabled);

const checkoutOptions = useMemo<CheckoutOptions | undefined>(() => {
if (!authToken) {
return undefined;
}
return {
authentication: {
token: authToken,
},
};
}, [authToken]);

const onAddressChangeIntent = (event: {id: string}) => {
navigation.navigate('Address', {id: event.id});
Expand Down
16 changes: 16 additions & 0 deletions sample/src/screens/SettingsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ function SettingsScreen() {
});
}, [appConfig, setAppConfig]);

const handleToggleAppAuthentication = useCallback(() => {
setAppConfig({
...appConfig,
appAuthenticationEnabled: !appConfig.appAuthenticationEnabled,
});
}, [appConfig, setAppConfig]);

const configurationOptions: readonly SwitchItem[] = useMemo(
() => [
{
Expand All @@ -148,12 +155,21 @@ function SettingsScreen() {
value: appConfig.customerAuthenticated,
handler: handleToggleCustomerAuthenticated,
},
{
title: 'App authentication',
description:
'Provide an app authentication token with checkout requests. Allows applying app specific checkout customizations and prevents redaction of checkout event data.',
type: SectionType.Switch,
value: appConfig.appAuthenticationEnabled,
handler: handleToggleAppAuthentication,
},
],
[
appConfig,
handleTogglePrefill,
handleTogglePreloading,
handleToggleCustomerAuthenticated,
handleToggleAppAuthentication,
],
);

Expand Down
Loading
Loading