Skip to content
Open
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 @@ -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
4 changes: 4 additions & 0 deletions sample/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,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>
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
105 changes: 35 additions & 70 deletions sample/src/screens/BuyNow/CheckoutScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,78 +19,39 @@ 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, {useEffect, useRef, useState} from 'react';
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {
Checkout,
type CheckoutRef,
type CheckoutOptions,
} from '@shopify/checkout-sheet-kit';
import Config from 'react-native-config';
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';

/**
* Response from Shopify's access token endpoint
*/
interface AccessTokenResponse {
access_token: string;
expires_in?: number;
token_type?: string;
}

/**
* Hook that fetches an authentication token from the authorization server.
*/
function useAuth(): string | undefined {
function useAuth(enabled: boolean): string | undefined {
const [token, setToken] = useState<string | undefined>();

useEffect(() => {
const fetchToken = async () => {
const clientId = Config.SHOPIFY_CLIENT_ID || '';
const clientSecret = Config.SHOPIFY_CLIENT_SECRET || '';
const authEndpoint = Config.SHOPIFY_AUTH_ENDPOINT || '';

// Skip if credentials are not configured
if (!clientId || !clientSecret) {
console.warn(
'SHOPIFY_CLIENT_ID or SHOPIFY_CLIENT_SECRET not configured',
);
return;
}

try {
const response = await fetch(
authEndpoint,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
grant_type: 'client_credentials',
}),
},
);

if (!response.ok) {
throw new Error(
`Failed to fetch access token: ${response.status} ${response.statusText}`,
);
}

const data: AccessTokenResponse = await response.json();
setToken(data.access_token);
} catch (error) {
console.error('Error fetching auth token:', error);
}
};

fetchToken();
}, []);


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;
}
Expand All @@ -102,15 +63,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 @@ -138,6 +138,13 @@ function SettingsScreen() {
});
}, [appConfig, setAppConfig]);

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

const configurationOptions: readonly SwitchItem[] = useMemo(
() => [
{
Expand All @@ -160,13 +167,22 @@ 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,
preloadingEnabled,
handleTogglePrefill,
handleTogglePreloading,
handleToggleCustomerAuthenticated,
handleToggleAppAuthentication,
],
);

Expand Down
Loading
Loading