From 0a92a954f51c010bf0ed267bb981900e11033a37 Mon Sep 17 00:00:00 2001 From: tiagocandido Date: Tue, 29 Jul 2025 16:59:42 +0200 Subject: [PATCH] Demo: New Checkout Component and inline mode --- .../@shopify/checkout-sheet-kit/package.json | 3 +- .../src/ShopifyCheckout.tsx | 468 ++++++++++++++++ .../@shopify/checkout-sheet-kit/src/index.ts | 6 + sample/ios/Podfile.lock | 62 ++- sample/package.json | 3 +- sample/src/App.tsx | 76 ++- sample/src/screens/ChatScreen.tsx | 394 ++++++++++++++ sample/src/screens/CheckoutKitDemoScreen.tsx | 503 ++++++++++++++++++ yarn.lock | 17 +- 9 files changed, 1517 insertions(+), 15 deletions(-) create mode 100644 modules/@shopify/checkout-sheet-kit/src/ShopifyCheckout.tsx create mode 100644 sample/src/screens/ChatScreen.tsx create mode 100644 sample/src/screens/CheckoutKitDemoScreen.tsx diff --git a/modules/@shopify/checkout-sheet-kit/package.json b/modules/@shopify/checkout-sheet-kit/package.json index 1602e715..cc5fa645 100644 --- a/modules/@shopify/checkout-sheet-kit/package.json +++ b/modules/@shopify/checkout-sheet-kit/package.json @@ -45,7 +45,8 @@ ], "peerDependencies": { "react": "*", - "react-native": "*" + "react-native": "*", + "react-native-webview": "*" }, "devDependencies": { "react-native-builder-bob": "^0.23.1", diff --git a/modules/@shopify/checkout-sheet-kit/src/ShopifyCheckout.tsx b/modules/@shopify/checkout-sheet-kit/src/ShopifyCheckout.tsx new file mode 100644 index 00000000..fe236dec --- /dev/null +++ b/modules/@shopify/checkout-sheet-kit/src/ShopifyCheckout.tsx @@ -0,0 +1,468 @@ +/* +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 React, { + forwardRef, + useImperativeHandle, + useEffect, + useState, + useRef, +} from 'react'; +import {View, StyleSheet, Text, TouchableOpacity} from 'react-native'; +import {useShopifyCheckoutSheet} from './context'; +import type {EmitterSubscription} from 'react-native'; + +export interface ShopifyCheckoutProps { + /** The checkout URL to load */ + url?: string; + /** Authentication token for inline mode (optional - for demo purposes) */ + auth?: string; + /** Display mode: 'popup' (default) | 'inline' */ + mode?: 'popup' | 'inline'; + /** Auto-resize height when in inline mode (default: true) */ + autoResizeHeight?: boolean; + /** Custom styling for the container */ + style?: any; + /** Called when checkout completes */ + onCompleted?: (event: any) => void; + /** Called when checkout encounters an error */ + onError?: (error: any) => void; + /** Called when checkout is closed/dismissed */ + onClose?: () => void; + /** Called when checkout size changes (inline mode only) */ + onResize?: (height: number) => void; +} + +export interface ShopifyCheckoutRef { + /** Open the checkout (popup mode only) */ + open: () => void; + /** Close/dismiss the checkout */ + close: () => void; + /** Preload the checkout for better performance */ + preload: () => void; +} + +// WebView Plugin Interface +export interface WebViewPlugin { + WebView: any; + createWebViewProps: (props: WebViewProps) => any; +} + +interface WebViewProps { + source: {uri: string}; + style?: any; + onMessage?: (event: any) => void; + onLoadEnd?: () => void; + onError?: (error: any) => void; + injectedJavaScript?: string; + onContentSizeChange?: (event: any) => void; +} + +// Global plugin registry +let webViewPlugin: WebViewPlugin | null = null; + +export function registerWebViewPlugin(plugin: WebViewPlugin) { + webViewPlugin = plugin; +} + +export function isWebViewAvailable(): boolean { + return webViewPlugin !== null; +} + +const ShopifyCheckout = forwardRef( + ( + { + url, + auth, + mode = 'popup', + autoResizeHeight = true, + style, + onCompleted, + onError, + onClose, + onResize, + }, + ref, + ) => { + const shopify = useShopifyCheckoutSheet(); + const [isVisible, setIsVisible] = useState(mode === 'inline'); + const [webViewHeight, setWebViewHeight] = useState(400); + const eventSubscriptions = useRef([]); + + // Event listeners setup + useEffect(() => { + const subscriptions: EmitterSubscription[] = []; + + if (onCompleted) { + const completedSub = shopify.addEventListener('completed', onCompleted); + if (completedSub) { + subscriptions.push(completedSub); + } + } + + if (onError) { + const errorSub = shopify.addEventListener('error', onError); + if (errorSub) { + subscriptions.push(errorSub); + } + } + + if (onClose) { + const closeSub = shopify.addEventListener('close', () => { + setIsVisible(false); + onClose(); + }); + if (closeSub) { + subscriptions.push(closeSub); + } + } + + eventSubscriptions.current = subscriptions; + + return () => { + subscriptions.forEach(sub => sub.remove()); + }; + }, [shopify, onCompleted, onError, onClose]); + + // Imperative API for ref + useImperativeHandle(ref, () => ({ + open: () => { + if (mode === 'popup' && url) { + shopify.present(url); + } else if (mode === 'inline') { + setIsVisible(true); + } + }, + close: () => { + if (mode === 'popup') { + shopify.dismiss(); + } else if (mode === 'inline') { + setIsVisible(false); + onClose?.(); + } + }, + preload: () => { + if (url) { + shopify.preload(url); + } + }, + })); + + // Build checkout URL with embed parameters for inline mode + const buildEmbedUrl = (checkoutUrl: string, authToken?: string): string => { + if (!authToken) { + // No authentication - use basic embed parameters + const embedParams = [ + 'branding=app', + 'platform=ReactNative', + 'entry=Inline', + 'protocol=2025-04', + ].join(','); + + const separator = checkoutUrl.includes('?') ? '&' : '?'; + return `${checkoutUrl}${separator}embed="${embedParams}"`; + } + + // With authentication + const embedParams = [ + `authentication=${authToken}`, + 'branding=app', + 'platform=ReactNative', + 'entry=Inline', + 'protocol=2025-04', + ].join(','); + + const separator = checkoutUrl.includes('?') ? '&' : '?'; + return `${checkoutUrl}${separator}embed="${embedParams}"`; + }; + + // Handle WebView messages for protocol communication + const handleWebViewMessage = (event: any) => { + try { + const message = JSON.parse(event.nativeEvent.data); + + switch (message.type) { + case 'checkout_completed': + onCompleted?.(message.payload); + setIsVisible(false); + break; + case 'checkout_error': + onError?.(message.payload); + break; + case 'checkout_resize': + if (autoResizeHeight && message.payload.height) { + setWebViewHeight(message.payload.height); + onResize?.(message.payload.height); + } + break; + case 'checkout_close': + setIsVisible(false); + onClose?.(); + break; + } + } catch (error) { + // eslint-disable-next-line no-console + console.warn('Failed to parse WebView message:', error); + } + }; + + // JavaScript to inject into WebView for protocol communication + const injectedJavaScript = ` + (function() { + // Protocol message handler + function sendMessage(type, payload) { + if (window.ReactNativeWebView) { + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: type, + payload: payload, + })); + } + } + + // Listen for checkout events + window.addEventListener('message', function(event) { + if (event.data && event.data.type) { + switch (event.data.type) { + case 'checkout_completed': + case 'checkout_error': + case 'checkout_close': + sendMessage(event.data.type, event.data.payload); + break; + } + } + }); + + // Auto-resize observer + if (${autoResizeHeight}) { + const resizeObserver = new ResizeObserver(function(entries) { + for (let entry of entries) { + const height = entry.contentRect.height; + if (height > 0) { + sendMessage('checkout_resize', { height: height }); + } + } + }); + + if (document.body) { + resizeObserver.observe(document.body); + } + } + + // Initial height check + setTimeout(() => { + const height = document.body?.scrollHeight || document.body?.offsetHeight; + if (height > 0 && ${autoResizeHeight}) { + sendMessage('checkout_resize', { height: height }); + } + }, 1000); + })(); + true; // Required for iOS + `; + + // Mock checkout completion for demo when WebView is not available + const handleMockComplete = () => { + if (onCompleted) { + onCompleted({ + orderDetails: { + id: 'mock-order-123', + total: '$99.99', + }, + }); + } + setIsVisible(false); + }; + + // Inline mode rendering + if (mode === 'inline') { + if (!isVisible || !url) { + return null; + } + + // Use WebView if plugin is available (auth is optional) + if (webViewPlugin) { + const {WebView, createWebViewProps} = webViewPlugin; + const embedUrl = buildEmbedUrl(url, auth); + + const webViewProps = createWebViewProps({ + source: {uri: embedUrl}, + style: [ + styles.webView, + autoResizeHeight ? {height: webViewHeight} : {flex: 1}, + ], + onMessage: handleWebViewMessage, + onError: (error: any) => { + // eslint-disable-next-line no-console + console.error('WebView error:', error); + onError?.(error); + }, + injectedJavaScript, + onContentSizeChange: autoResizeHeight + ? (event: any) => { + const {height} = event.nativeEvent.contentSize; + if (height > 0) { + setWebViewHeight(height); + onResize?.(height); + } + } + : undefined, + }); + + return ( + + + + ); + } + + // Fallback to mock implementation when WebView is not available + return ( + + + ๐Ÿ›’ Shopify Checkout + + {webViewPlugin ? 'WebView Enhanced' : 'Mock Implementation'} + + URL: {url} + Mode: {mode} + + Auth: {auth ? 'โœ… Provided' : 'โšช Optional'} + + + {!webViewPlugin && ( + + {`โš ๏ธ WebView not available. To enable real checkout, register WebView plugin: + +import {WebView} from 'react-native-webview'; +import {registerWebViewPlugin} from '@shopify/checkout-sheet-kit'; + +registerWebViewPlugin({ + WebView, + createWebViewProps: (props) => props +});`} + + )} + + + + Complete Mock Order + + + { + setIsVisible(false); + onClose?.(); + }}> + Close + + + + + ); + } + + // Popup mode - render nothing (checkout presented natively) + return null; + }, +); + +const styles = StyleSheet.create({ + inlineContainer: { + backgroundColor: '#ffffff', + borderRadius: 8, + overflow: 'hidden', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 3.84, + elevation: 5, + minHeight: 400, + }, + webView: { + backgroundColor: 'transparent', + }, + mockCheckout: { + padding: 20, + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + title: { + fontSize: 24, + fontWeight: 'bold', + marginBottom: 8, + textAlign: 'center', + }, + subtitle: { + fontSize: 18, + color: '#007AFF', + marginBottom: 20, + textAlign: 'center', + }, + info: { + fontSize: 14, + color: '#666', + marginBottom: 8, + textAlign: 'center', + }, + warning: { + fontSize: 12, + color: '#FF9500', + backgroundColor: '#FFF5E6', + padding: 12, + borderRadius: 6, + marginVertical: 10, + textAlign: 'center', + fontFamily: 'Menlo, Monaco, monospace', + }, + buttonContainer: { + flexDirection: 'row', + marginTop: 20, + gap: 10, + }, + completeButton: { + backgroundColor: '#34C759', + paddingHorizontal: 20, + paddingVertical: 10, + borderRadius: 8, + }, + closeButton: { + backgroundColor: '#FF3B30', + paddingHorizontal: 20, + paddingVertical: 10, + borderRadius: 8, + }, + buttonText: { + color: 'white', + fontWeight: '600', + }, +}); + +ShopifyCheckout.displayName = 'ShopifyCheckout'; + +export default ShopifyCheckout; diff --git a/modules/@shopify/checkout-sheet-kit/src/index.ts b/modules/@shopify/checkout-sheet-kit/src/index.ts index 4dda85a7..e3d7848b 100644 --- a/modules/@shopify/checkout-sheet-kit/src/index.ts +++ b/modules/@shopify/checkout-sheet-kit/src/index.ts @@ -381,6 +381,12 @@ export { useShopifyCheckoutSheet, }; +// New React Component API +export {default as ShopifyCheckout} from './ShopifyCheckout'; +export type {ShopifyCheckoutProps, ShopifyCheckoutRef} from './ShopifyCheckout'; +export {registerWebViewPlugin, isWebViewAvailable} from './ShopifyCheckout'; +export type {WebViewPlugin} from './ShopifyCheckout'; + // Error classes export { CheckoutClientError, diff --git a/sample/ios/Podfile.lock b/sample/ios/Podfile.lock index cf0a9a36..7ab29693 100644 --- a/sample/ios/Podfile.lock +++ b/sample/ios/Podfile.lock @@ -935,12 +935,33 @@ PODS: - React-Mapbuffer (0.74.1): - glog - React-debug - - react-native-config (1.5.3): - - react-native-config/App (= 1.5.3) - - react-native-config/App (1.5.3): + - react-native-config (1.5.5): + - react-native-config/App (= 1.5.5) + - react-native-config/App (1.5.5): - React-Core - react-native-safe-area-context (4.14.0): - React-Core + - react-native-webview (13.15.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - React-nativeconfig (0.74.1) - React-NativeModulesApple (0.74.1): - glog @@ -1170,8 +1191,27 @@ PODS: - React-logger (= 0.74.1) - React-perflogger (= 0.74.1) - React-utils (= 0.74.1) - - RNCMaskedView (0.3.1): + - RNCMaskedView (0.3.2): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - RNGestureHandler (2.15.0): - DoubleConversion - glog @@ -1302,7 +1342,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNShopifyCheckoutSheetKit (3.1.2): + - RNShopifyCheckoutSheetKit (3.2.0): - React-Core - ShopifyCheckoutSheetKit (~> 3.1.2) - RNVectorIcons (10.2.0): @@ -1366,6 +1406,7 @@ DEPENDENCIES: - React-Mapbuffer (from `../../node_modules/react-native/ReactCommon`) - react-native-config (from `../node_modules/react-native-config`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - react-native-webview (from `../node_modules/react-native-webview`) - React-nativeconfig (from `../../node_modules/react-native/ReactCommon`) - React-NativeModulesApple (from `../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-perflogger (from `../../node_modules/react-native/ReactCommon/reactperflogger`) @@ -1470,6 +1511,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-config" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" + react-native-webview: + :path: "../node_modules/react-native-webview" React-nativeconfig: :path: "../../node_modules/react-native/ReactCommon" React-NativeModulesApple: @@ -1562,8 +1605,9 @@ SPEC CHECKSUMS: React-jsitracing: dd0e541a34027b3ab668ad94cf268482ad6f82fb React-logger: 6070f362a1657bb53335eb1fc903d3f49fd79842 React-Mapbuffer: 2c95cbabc3d75a17747452381e998c35208ea3ee - react-native-config: ea75335a7cca1d3326de1da384227e580a7c082e + react-native-config: 644074ab88db883fcfaa584f03520ec29589d7df react-native-safe-area-context: b13be9714d9771fbde0120bc519c963484de3a71 + react-native-webview: 3fbd0ebc6fee8e1f5c0fc1a1dae5252468e59969 React-nativeconfig: b0073a590774e8b35192fead188a36d1dca23dec React-NativeModulesApple: 61b07ab32af3ea4910ba553932c0a779e853c082 React-perflogger: 3d31e0d1e8ad891e43a09ac70b7b17a79773003a @@ -1587,11 +1631,11 @@ SPEC CHECKSUMS: React-runtimescheduler: 87b14969bb0b10538014fb8407d472f9904bc8cd React-utils: 67574b07bff4429fd6c4d43a7fad8254d814ee20 ReactCommon: 64c64f4ae1f2debe3fab1800e00cb8466a4477b7 - RNCMaskedView: de80352547bd4f0d607bf6bab363d826822bd126 + RNCMaskedView: af7c1703f39cdef08a99275bfcadf324aa403403 RNGestureHandler: 293aea360e79439e2272b8a5ffebd582a1e4c486 RNReanimated: af5545657216ca1794252c132f9e6e8ceb475462 RNScreens: 02747ebee17d2e322af4ee383877367cf82a3cd6 - RNShopifyCheckoutSheetKit: 26cb201d8ef66263aaec45c2bfa9649606576a2a + RNShopifyCheckoutSheetKit: 3c10482e626a3e2579e729e61becef5cca3af859 RNVectorIcons: 6c795cacc9276decc31d8e1a139b9cc6fc0479ca ShopifyCheckoutSheetKit: 5ae02dbed0047689b94c977bdcf6287752d17ce4 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d @@ -1600,4 +1644,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 9efd19a381198fb46f36acf3d269233039fb9dc5 -COCOAPODS: 1.16.1 +COCOAPODS: 1.16.2 diff --git a/sample/package.json b/sample/package.json index 0729f30c..b43df94e 100644 --- a/sample/package.json +++ b/sample/package.json @@ -30,7 +30,8 @@ "react-native-reanimated": "^3.16.1", "react-native-safe-area-context": "^4.14.0", "react-native-screens": "^4.1.0", - "react-native-vector-icons": "^10.2.0" + "react-native-vector-icons": "^10.2.0", + "react-native-webview": "^13.15.0" }, "peerDependencies": { "@types/react-native": "*", diff --git a/sample/src/App.tsx b/sample/src/App.tsx index 4bc9947d..e647b321 100644 --- a/sample/src/App.tsx +++ b/sample/src/App.tsx @@ -32,19 +32,30 @@ import { } from '@react-navigation/native'; import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; import {createNativeStackNavigator} from '@react-navigation/native-stack'; -import {ApolloClient, InMemoryCache, ApolloProvider} from '@apollo/client'; +import { + ApolloClient, + InMemoryCache, + ApolloProvider, + from, + createHttpLink, +} from '@apollo/client'; +import {onError} from '@apollo/client/link/error'; import env from 'react-native-config'; import Icon from 'react-native-vector-icons/Entypo'; import CatalogScreen from './screens/CatalogScreen'; import SettingsScreen from './screens/SettingsScreen'; +import ChatScreen from './screens/ChatScreen'; +import CheckoutKitDemoScreen from './screens/CheckoutKitDemoScreen'; import type {Configuration} from '@shopify/checkout-sheet-kit'; import { ColorScheme, ShopifyCheckoutSheetProvider, useShopifyCheckoutSheet, + registerWebViewPlugin, } from '@shopify/checkout-sheet-kit'; +import {WebView} from 'react-native-webview'; import type { CheckoutCompletedEvent, CheckoutException, @@ -77,6 +88,12 @@ const config: Configuration = { }, }; +// Register WebView plugin for inline checkout support +registerWebViewPlugin({ + WebView, + createWebViewProps: props => props, +}); + export type RootStackParamList = { Catalog: undefined; CatalogScreen: undefined; @@ -84,6 +101,8 @@ export type RootStackParamList = { Cart: undefined; CartModal: undefined; Settings: undefined; + Chat: undefined; + Demo: undefined; }; const Tab = createBottomTabNavigator(); @@ -91,14 +110,49 @@ const Stack = createNativeStackNavigator(); export const cache = new InMemoryCache(); -const client = new ApolloClient({ +const errorLink = onError(({graphQLErrors, networkError, operation}) => { + if (graphQLErrors) { + graphQLErrors.forEach(({message, locations, path}) => + console.log( + `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`, + ), + ); + } + + if (networkError) { + console.log(`[Network error]: ${networkError}`); + console.log('Request:', operation.operationName, operation.variables); + console.log( + 'URI:', + `https://${env.STOREFRONT_DOMAIN}/api/${env.STOREFRONT_VERSION}/graphql.json`, + ); + console.log('Headers:', { + 'Content-Type': 'application/json', + 'X-Shopify-Storefront-Access-Token': env.STOREFRONT_ACCESS_TOKEN ?? '', + }); + } +}); + +const httpLink = createHttpLink({ uri: `https://${env.STOREFRONT_DOMAIN}/api/${env.STOREFRONT_VERSION}/graphql.json`, - cache, headers: { 'Content-Type': 'application/json', 'X-Shopify-Storefront-Access-Token': env.STOREFRONT_ACCESS_TOKEN ?? '', }, +}); + +const client = new ApolloClient({ + cache, + link: from([errorLink, httpLink]), connectToDevTools: true, + defaultOptions: { + watchQuery: { + errorPolicy: 'all', + }, + query: { + errorPolicy: 'all', + }, + }, }); function AppWithTheme({children}: PropsWithChildren) { @@ -326,6 +380,22 @@ function Routes() { tabBarBadge: totalQuantity > 0 ? totalQuantity : undefined, }} /> + + { + const [messages, setMessages] = useState([ + { + id: '1', + text: "Hi! I'm your shopping assistant. I can help you browse products and complete your purchase. Try saying 'show me my cart' or 'help me checkout'!", + isUser: false, + timestamp: new Date(), + }, + ]); + const [inputText, setInputText] = useState(''); + const [showInlineCheckout, setShowInlineCheckout] = useState(false); + const {checkoutURL} = useCart(); + const checkoutRef = useRef(null); + + // Mock auth token - in a real app, this would come from your authentication system + // Authentication is now optional for demo purposes + // const mockAuthToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcGlfa2V5IjoibW9jay1hcGkta2V5IiwidmFyaWFudCI6Im1vY2stdmFyaWFudCIsImlhdCI6MTY0MDk5NTIwMCwiZXhwIjoxNjQwOTk4ODAwfQ.mock-signature'; + + const addMessage = (text: string, isUser: boolean, isCheckout = false) => { + const newMessage: ChatMessage = { + id: Date.now().toString(), + text, + isUser, + timestamp: new Date(), + isCheckout, + }; + setMessages(prev => [...prev, newMessage]); + }; + + const handleSendMessage = () => { + if (!inputText.trim()) { + return; + } + + const userMessage = inputText.trim(); + addMessage(userMessage, true); + setInputText(''); + + // Simulate bot responses + setTimeout(() => { + handleBotResponse(userMessage.toLowerCase()); + }, 500); + }; + + const handleBotResponse = (userMessage: string) => { + if (userMessage.includes('cart') || userMessage.includes('checkout')) { + if (checkoutURL) { + addMessage( + "I can help you complete your purchase! Here's your checkout embedded right in our chat:", + false, + ); + setShowInlineCheckout(true); + addMessage('', false, true); // Special checkout message + } else { + addMessage( + 'Your cart is empty. Add some products first, then I can help you checkout!', + false, + ); + } + } else if (userMessage.includes('hello') || userMessage.includes('hi')) { + addMessage('Hello! How can I help you with your shopping today?', false); + } else if (userMessage.includes('help')) { + addMessage( + 'I can help you with:\nโ€ข Viewing your cart\nโ€ข Completing checkout\nโ€ข Finding products\nโ€ข Answering questions', + false, + ); + } else if (userMessage.includes('popup') || userMessage.includes('sheet')) { + addMessage('Opening checkout in a popup window for you!', false); + setTimeout(() => { + if (checkoutURL) { + checkoutRef.current?.open(); + } + }, 500); + } else { + addMessage( + "I understand you said: '" + + userMessage + + "'. Try asking about your cart or checkout!", + false, + ); + } + }; + + const handleCheckoutCompleted = (event: any) => { + setShowInlineCheckout(false); + addMessage( + `๐ŸŽ‰ Order completed successfully! Order #${event.orderDetails?.id || 'unknown'}`, + false, + ); + Alert.alert('Success', 'Your order has been completed!'); + }; + + const handleCheckoutError = (error: any) => { + // Handle authentication errors specifically + if (error.message && error.message.toLowerCase().includes('unauthorized')) { + addMessage( + "๐Ÿ” Authentication failed with the demo token. This is expected when using mock authentication with real Shopify URLs. In production, you'd use a real JWT token from your authentication service.", + false, + ); + Alert.alert( + 'Demo Authentication', + 'This demonstrates WebView integration. Real auth tokens would be needed for production.', + ); + } else { + addMessage( + `โŒ Checkout encountered an error: ${error.message || 'Unknown error'}`, + false, + ); + Alert.alert('Error', 'There was a problem with your checkout.'); + } + }; + + const handleCheckoutClose = () => { + setShowInlineCheckout(false); + addMessage('Checkout was closed. Let me know if you need help!', false); + }; + + const renderMessage = (message: ChatMessage) => { + if (message.isCheckout && showInlineCheckout && checkoutURL) { + return ( + + { + console.log('Checkout resized to:', height); + }} + /> + + ); + } + + return ( + + + {message.text} + + + {message.timestamp.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + })} + + + ); + }; + + return ( + + {/* Hidden popup checkout component */} + addMessage('Popup checkout was closed.', false)} + /> + + + Shopping Assistant + + Checkout Kit Demo - Inline & Popup + + + + + {messages.map(renderMessage)} + + {/* Demo instructions */} + + Try these commands: + + โ€ข "show me my cart" - Shows inline checkout + + + โ€ข "popup checkout" - Opens popup checkout + + + โ€ข "help" - Shows available commands + + + + + + + + Send + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, + header: { + backgroundColor: '#007AFF', + paddingVertical: 16, + paddingHorizontal: 20, + borderBottomWidth: 1, + borderBottomColor: '#e0e0e0', + }, + headerTitle: { + fontSize: 18, + fontWeight: 'bold', + color: 'white', + }, + headerSubtitle: { + fontSize: 14, + color: 'rgba(255, 255, 255, 0.8)', + marginTop: 2, + }, + messagesContainer: { + flex: 1, + paddingHorizontal: 16, + paddingVertical: 8, + }, + messageContainer: { + marginVertical: 4, + maxWidth: '80%', + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 16, + }, + userMessage: { + alignSelf: 'flex-end', + backgroundColor: '#007AFF', + }, + botMessage: { + alignSelf: 'flex-start', + backgroundColor: 'white', + borderWidth: 1, + borderColor: '#e0e0e0', + }, + messageText: { + fontSize: 16, + lineHeight: 20, + }, + userMessageText: { + color: 'white', + }, + botMessageText: { + color: '#333', + }, + timestamp: { + fontSize: 12, + color: '#999', + marginTop: 4, + alignSelf: 'flex-end', + }, + checkoutContainer: { + marginVertical: 8, + backgroundColor: 'white', + borderRadius: 12, + padding: 8, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 3.84, + elevation: 5, + }, + inlineCheckout: { + minHeight: 400, + borderRadius: 8, + }, + instructionsContainer: { + backgroundColor: 'rgba(0, 122, 255, 0.1)', + padding: 16, + borderRadius: 12, + marginVertical: 16, + marginHorizontal: 8, + }, + instructionsTitle: { + fontSize: 16, + fontWeight: 'bold', + color: '#007AFF', + marginBottom: 8, + }, + instructionText: { + fontSize: 14, + color: '#666', + marginBottom: 4, + }, + inputContainer: { + flexDirection: 'row', + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: 'white', + borderTopWidth: 1, + borderTopColor: '#e0e0e0', + alignItems: 'flex-end', + }, + textInput: { + flex: 1, + borderWidth: 1, + borderColor: '#e0e0e0', + borderRadius: 20, + paddingHorizontal: 16, + paddingVertical: 10, + marginRight: 8, + maxHeight: 100, + fontSize: 16, + }, + sendButton: { + backgroundColor: '#007AFF', + paddingHorizontal: 20, + paddingVertical: 12, + borderRadius: 20, + }, + sendButtonText: { + color: 'white', + fontWeight: '600', + fontSize: 16, + }, +}); + +export default ChatScreen; diff --git a/sample/src/screens/CheckoutKitDemoScreen.tsx b/sample/src/screens/CheckoutKitDemoScreen.tsx new file mode 100644 index 00000000..a26d97e9 --- /dev/null +++ b/sample/src/screens/CheckoutKitDemoScreen.tsx @@ -0,0 +1,503 @@ +/* +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 React, {useRef, useState} from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + Alert, + SafeAreaView, +} from 'react-native'; +import { + useShopifyCheckoutSheet, + ShopifyCheckout, +} from '@shopify/checkout-sheet-kit'; +import type {ShopifyCheckoutRef} from '@shopify/checkout-sheet-kit'; +import {useCart} from '../context/Cart'; + +const CheckoutKitDemoScreen: React.FC = () => { + const shopify = useShopifyCheckoutSheet(); + const {checkoutURL} = useCart(); + const [showInlineCheckout, setShowInlineCheckout] = useState(false); + + // Refs for the new component API + const popupCheckoutRef = useRef(null); + + // Mock auth token - OPTIONAL for demo purposes + // const mockAuthToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcGlfa2V5IjoibW9jay1hcGkta2V5IiwidmFyaWFudCI6Im1vY2stdmFyaWFudCIsImlhdCI6MTY0MDk5NTIwMCwiZXhwIjoxNjQwOTk4ODAwfQ.mock-signature'; + + // Demo checkout URL for testing when cart is empty + const demoCheckoutURL = + 'https://shopify.github.io/checkout-sheet-kit-react-native/demo-checkout'; + + // Use real checkout URL if available, otherwise use demo URL + const effectiveCheckoutURL = checkoutURL || demoCheckoutURL; + const isUsingDemoURL = !checkoutURL; + + const handleOldAPIPresent = () => { + if (effectiveCheckoutURL) { + if (isUsingDemoURL) { + Alert.alert( + 'Demo Mode', + 'Using demo checkout URL. Add items to cart for real checkout.', + [ + { + text: 'Continue', + onPress: () => shopify.present(effectiveCheckoutURL), + }, + ], + ); + } else { + shopify.present(effectiveCheckoutURL); + } + } + }; + + const handleNewAPIPopup = () => { + if (effectiveCheckoutURL) { + if (isUsingDemoURL) { + Alert.alert( + 'Demo Mode', + 'Using demo checkout URL. Add items to cart for real checkout.', + [{text: 'Continue', onPress: () => popupCheckoutRef.current?.open()}], + ); + } else { + popupCheckoutRef.current?.open(); + } + } + }; + + const handleNewAPIInline = () => { + if (effectiveCheckoutURL) { + if (isUsingDemoURL && !showInlineCheckout) { + Alert.alert( + 'Demo Mode', + 'Using demo checkout URL without authentication. This demonstrates the inline WebView integration working with optional authentication.', + [ + {text: 'Cancel', style: 'cancel'}, + { + text: 'Show Demo', + onPress: () => setShowInlineCheckout(!showInlineCheckout), + }, + ], + ); + } else { + setShowInlineCheckout(!showInlineCheckout); + } + } + }; + + const handlePreload = () => { + if (effectiveCheckoutURL) { + shopify.preload(effectiveCheckoutURL); + // Also preload via new API + popupCheckoutRef.current?.preload(); + Alert.alert( + 'Preloaded', + `Checkout has been preloaded for better performance${isUsingDemoURL ? ' (demo URL)' : ''}`, + ); + } + }; + + const handleCheckoutCompleted = (event: any) => { + console.log('Checkout completed:', event); + Alert.alert( + 'Order Complete! ๐ŸŽ‰', + `Order ID: ${event.orderDetails?.id || 'unknown'}${isUsingDemoURL ? ' (Demo)' : ''}`, + ); + setShowInlineCheckout(false); + }; + + const handleCheckoutError = (error: any) => { + console.log('Checkout error:', error); + + if (isUsingDemoURL) { + Alert.alert( + 'Demo Checkout', + 'Demo checkout loaded successfully! In a real app, this would be a real Shopify checkout URL.', + ); + } else { + Alert.alert('Checkout Error', error.message || 'Unknown error occurred'); + } + }; + + const handleCheckoutClose = () => { + console.log('Checkout closed'); + setShowInlineCheckout(false); + }; + + const handleResize = (height: number) => { + console.log('Checkout resized to height:', height); + }; + + return ( + + {/* Hidden popup checkout using new API */} + + + + + Checkout Kit API Comparison + + Compare the old hook-based API with the new component-based API + + + + {/* Old API Section */} + + ๐Ÿ“ฑ Legacy Hook API + Usage: + + + {`const shopify = useShopifyCheckoutSheet(); +shopify.present(checkoutUrl);`} + + + + + Present Checkout (Legacy) + + + + {/* New API Popup Section */} + + ๐Ÿ†• New Component API - Popup + Usage: + + + {`const checkout = useRef(); + + + +checkout.current?.open();`} + + + + + Present Checkout (New API) + + + + {/* New API Inline Section */} + + ๐Ÿ”— New Component API - Inline + Usage: + + + {``} + + + + + + {showInlineCheckout ? 'Hide' : 'Show'} Inline Checkout + + + + + {/* Performance Section */} + + โšก Performance Features + + Preload checkout for faster presentation + + + + Preload Checkout + + + + {/* Inline Checkout Display */} + {showInlineCheckout && effectiveCheckoutURL && ( + + ๐Ÿ’ณ Inline Checkout + {isUsingDemoURL && ( + + ๐Ÿงช Demo Mode: Using mock URL and authentication + + )} + + + )} + + {/* Features Comparison */} + + ๐Ÿ“Š Feature Comparison + + + Feature + Legacy + New API + + + Popup/Sheet Mode + โœ… + โœ… + + + Inline Mode + โŒ + โœ… + + + Ref-based Control + โŒ + โœ… + + + Auto-resize + โŒ + โœ… + + + Authentication + โŒ + โœ… + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f8f9fa', + }, + scrollView: { + flex: 1, + }, + section: { + padding: 20, + backgroundColor: '#007AFF', + }, + sectionTitle: { + fontSize: 24, + fontWeight: 'bold', + color: 'white', + marginBottom: 4, + }, + sectionSubtitle: { + fontSize: 16, + color: 'rgba(255, 255, 255, 0.8)', + }, + apiSection: { + backgroundColor: 'white', + margin: 16, + padding: 20, + borderRadius: 12, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 3.84, + elevation: 5, + }, + apiTitle: { + fontSize: 18, + fontWeight: 'bold', + color: '#333', + marginBottom: 12, + }, + codeLabel: { + fontSize: 14, + fontWeight: '600', + color: '#666', + marginBottom: 8, + }, + codeBlock: { + backgroundColor: '#f5f5f5', + padding: 12, + borderRadius: 8, + marginBottom: 16, + borderLeftWidth: 4, + borderLeftColor: '#007AFF', + }, + codeText: { + fontFamily: 'Courier', + fontSize: 12, + color: '#333', + lineHeight: 16, + }, + description: { + fontSize: 14, + color: '#666', + marginBottom: 16, + }, + actionButton: { + backgroundColor: '#007AFF', + paddingVertical: 12, + paddingHorizontal: 24, + borderRadius: 8, + alignItems: 'center', + }, + activeButton: { + backgroundColor: '#FF3B30', + }, + secondaryButton: { + backgroundColor: '#34C759', + paddingVertical: 12, + paddingHorizontal: 24, + borderRadius: 8, + alignItems: 'center', + }, + buttonText: { + color: 'white', + fontSize: 16, + fontWeight: '600', + }, + inlineSection: { + backgroundColor: 'white', + margin: 16, + padding: 20, + borderRadius: 12, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 3.84, + elevation: 5, + }, + inlineCheckout: { + minHeight: 400, + marginTop: 12, + borderRadius: 8, + }, + demoNotice: { + fontSize: 14, + color: '#FF9500', + backgroundColor: '#FFF5E6', + padding: 12, + borderRadius: 6, + marginBottom: 12, + textAlign: 'center', + fontWeight: '500', + }, + comparisonSection: { + backgroundColor: 'white', + margin: 16, + padding: 20, + borderRadius: 12, + marginBottom: 32, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 3.84, + elevation: 5, + }, + comparisonTable: { + marginTop: 12, + }, + comparisonRow: { + flexDirection: 'row', + paddingVertical: 8, + borderBottomWidth: 1, + borderBottomColor: '#f0f0f0', + }, + featureLabel: { + flex: 2, + fontSize: 14, + fontWeight: 'bold', + color: '#333', + }, + legacyLabel: { + flex: 1, + fontSize: 14, + fontWeight: 'bold', + color: '#666', + textAlign: 'center', + }, + newLabel: { + flex: 1, + fontSize: 14, + fontWeight: 'bold', + color: '#007AFF', + textAlign: 'center', + }, + featureText: { + flex: 2, + fontSize: 14, + color: '#333', + }, + checkmark: { + flex: 1, + fontSize: 16, + textAlign: 'center', + }, + cross: { + flex: 1, + fontSize: 16, + textAlign: 'center', + }, +}); + +export default CheckoutKitDemoScreen; diff --git a/yarn.lock b/yarn.lock index 3b98d42b..f1e46c36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3671,6 +3671,7 @@ __metadata: peerDependencies: react: "*" react-native: "*" + react-native-webview: "*" languageName: unknown linkType: soft @@ -7196,7 +7197,7 @@ __metadata: languageName: node linkType: hard -"invariant@npm:^2.2.4": +"invariant@npm:2.2.4, invariant@npm:^2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" dependencies: @@ -10137,6 +10138,19 @@ __metadata: languageName: node linkType: hard +"react-native-webview@npm:^13.15.0": + version: 13.15.0 + resolution: "react-native-webview@npm:13.15.0" + dependencies: + escape-string-regexp: "npm:^4.0.0" + invariant: "npm:2.2.4" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10c0/29b8ad4c1c8623c62e868893e829d20ef1ab51b38f44d0f422e0c19df9d8747ae25d1fcceec4e6d78106f263dce66b7984eeb12c0e5046c7c898b81673b0b6d6 + languageName: node + linkType: hard + "react-native@npm:0.74.1": version: 0.74.1 resolution: "react-native@npm:0.74.1" @@ -10646,6 +10660,7 @@ __metadata: react-native-safe-area-context: "npm:^4.14.0" react-native-screens: "npm:^4.1.0" react-native-vector-icons: "npm:^10.2.0" + react-native-webview: "npm:^13.15.0" peerDependencies: "@types/react-native": "*" react: "*"