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
65 changes: 63 additions & 2 deletions src/contexts/SocketClusterContext.native.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
import React, { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react';
import socketClusterClient from 'socketcluster-client';
import DeviceInfo from 'react-native-device-info';
import { config, toBoolean, consumeAsyncIterator } from '../utils';
import { SocketAuthManager, getCustomerToken } from '../utils/socket-auth';
import { useAuth } from './AuthContext';

const getClientTag = () => {
try {
return `storefront-app/${DeviceInfo.getVersion()}`;
} catch (_) {
return 'storefront-app';
}
};

const SocketClusterContext = createContext(null);

Expand All @@ -12,6 +23,12 @@ export const SocketClusterProvider = ({ children }) => {
const [socket, setSocket] = useState(null);
const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState(null);
const { customer } = useAuth();
const customerRef = useRef(customer);
customerRef.current = customer;
const authRef = useRef<SocketAuthManager | null>(null);
const customerId = customer?.id ?? null;
const previousCustomerId = useRef(customerId);

useEffect(() => {
// Initialize the socket connection
Expand All @@ -22,7 +39,17 @@ export const SocketClusterProvider = ({ children }) => {
secure: toBoolean(config('SOCKETCLUSTER_SECURE', true)),
};

const scSocket = socketClusterClient.create(options);
// Socket auth: the token lives in memory only and is delivered in the
// handshake by this authEngine (never storage, never the URL query).
const auth = new SocketAuthManager({ getCustomerToken: () => getCustomerToken(customerRef.current) });
authRef.current = auth;

const scSocket = socketClusterClient.create({
...options,
authEngine: auth.authEngine,
query: { client: getClientTag() },
});
auth.attach(scSocket);

// Define handlers for socket events
const handleConnect = () => {
Expand Down Expand Up @@ -59,11 +86,39 @@ export const SocketClusterProvider = ({ children }) => {
stopDisconnect();
stopError();

auth.destroy();
authRef.current = null;
scSocket.disconnect();
console.log('Socket connection closed.');
};
}, []);

// Re-authenticate the socket when the customer logs in, out or switches account.
useEffect(() => {
if (previousCustomerId.current === customerId) return;
previousCustomerId.current = customerId;
const auth = authRef.current;
if (!auth) return;
if (customerId) {
auth.onLogin().catch((err) => console.warn('Socket re-authentication failed:', err));
} else {
auth.onLogout();
}
}, [customerId]);

/**
* Authenticate the socket with a checkout-scoped token (guest checkout).
* No-op when a customer is logged in: their own token covers their checkouts.
*/
const authenticateWithCheckoutToken = useCallback(async (socketToken) => {
if (!authRef.current) return;
try {
await authRef.current.applyCheckoutToken(socketToken);
} catch (err) {
console.warn('Unable to authenticate socket with checkout token:', err);
}
}, []);

/**
* Subscribes to a specific channel.
* @param {string} channelName - The name of the channel to subscribe to.
Expand All @@ -78,6 +133,7 @@ export const SocketClusterProvider = ({ children }) => {

try {
const channel = socket.subscribe(channelName);
authRef.current?.track(channelName);
if (channel.isSubscribed()) {
console.log(`Already subscribed to channel "${channelName}".`);
return channel;
Expand Down Expand Up @@ -106,6 +162,7 @@ export const SocketClusterProvider = ({ children }) => {
}

try {
authRef.current?.untrack(channelName);
await socket.closeChannel(channelName);
console.log(`Gracefully closed channel "${channelName}".`);
} catch (err) {
Expand All @@ -127,6 +184,7 @@ export const SocketClusterProvider = ({ children }) => {
}

try {
authRef.current?.untrack(channelName);
await socket.killChannel(channelName);
console.log(`Forcefully killed channel "${channelName}".`);
} catch (err) {
Expand All @@ -146,6 +204,7 @@ export const SocketClusterProvider = ({ children }) => {
}

try {
authRef.current?.untrackAll();
await socket.closeAllChannels();
console.log('Gracefully closed all channels.');
} catch (err) {
Expand All @@ -163,6 +222,7 @@ export const SocketClusterProvider = ({ children }) => {
}

try {
authRef.current?.untrackAll();
await socket.killAllChannels();
console.log('Forcefully killed all channels.');
} catch (err) {
Expand All @@ -181,6 +241,7 @@ export const SocketClusterProvider = ({ children }) => {
killChannel,
closeAllChannels,
killAllChannels,
authenticateWithCheckoutToken,
}}
>
{children}
Expand Down
64 changes: 62 additions & 2 deletions src/contexts/SocketClusterContext.web.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
import React, { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react';
import socketClusterClient from 'socketcluster-client';
import { config, toBoolean } from '../utils';
import { SocketAuthManager, getCustomerToken } from '../utils/socket-auth';
import { useAuth } from './AuthContext';

const getClientTag = () => {
try {
return `storefront-app/${require('../../package.json').version}`;
} catch (_) {
return 'storefront-app';
}
};

const SocketClusterContext = createContext(null);

export const SocketClusterProvider = ({ children }) => {
const [socket, setSocket] = useState(null);
const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState(null);
const { customer } = useAuth();
const customerRef = useRef(customer);
customerRef.current = customer;
const authRef = useRef<SocketAuthManager | null>(null);
const customerId = customer?.id ?? null;
const previousCustomerId = useRef(customerId);

useEffect(() => {
const options = {
Expand All @@ -17,7 +33,17 @@ export const SocketClusterProvider = ({ children }) => {
secure: toBoolean(config('SOCKETCLUSTER_SECURE', true)),
};

const scSocket = socketClusterClient.create(options);
// Socket auth: the token lives in memory only and is delivered in the
// handshake by this authEngine (never storage, never the URL query).
const auth = new SocketAuthManager({ getCustomerToken: () => getCustomerToken(customerRef.current) });
authRef.current = auth;

const scSocket = socketClusterClient.create({
...options,
authEngine: auth.authEngine,
query: { client: getClientTag() },
});
auth.attach(scSocket);

// Listen for socket events using async iterators
(async () => {
Expand Down Expand Up @@ -56,11 +82,39 @@ export const SocketClusterProvider = ({ children }) => {
setSocket(scSocket);

return () => {
auth.destroy();
authRef.current = null;
scSocket.disconnect();
console.log('Socket connection closed.');
};
}, []);

// Re-authenticate the socket when the customer logs in, out or switches account.
useEffect(() => {
if (previousCustomerId.current === customerId) return;
previousCustomerId.current = customerId;
const auth = authRef.current;
if (!auth) return;
if (customerId) {
auth.onLogin().catch((err) => console.warn('Socket re-authentication failed:', err));
} else {
auth.onLogout();
}
}, [customerId]);

/**
* Authenticate the socket with a checkout-scoped token (guest checkout).
* No-op when a customer is logged in: their own token covers their checkouts.
*/
const authenticateWithCheckoutToken = useCallback(async (socketToken) => {
if (!authRef.current) return;
try {
await authRef.current.applyCheckoutToken(socketToken);
} catch (err) {
console.warn('Unable to authenticate socket with checkout token:', err);
}
}, []);

/**
* Subscribes to a channel and listens for its events using async iterators.
* Returns the channel if successful.
Expand All @@ -73,6 +127,7 @@ export const SocketClusterProvider = ({ children }) => {
}
try {
const channel = socket.subscribe(channelName);
authRef.current?.track(channelName);

// Listen for the subscription confirmation.
(async () => {
Expand Down Expand Up @@ -115,6 +170,7 @@ export const SocketClusterProvider = ({ children }) => {
return;
}
try {
authRef.current?.untrack(channelName);
await socket.closeChannel(channelName);
console.log(`Gracefully closed channel "${channelName}".`);
} catch (err) {
Expand All @@ -131,6 +187,7 @@ export const SocketClusterProvider = ({ children }) => {
return;
}
try {
authRef.current?.untrack(channelName);
await socket.killChannel(channelName);
console.log(`Forcefully killed channel "${channelName}".`);
} catch (err) {
Expand All @@ -146,6 +203,7 @@ export const SocketClusterProvider = ({ children }) => {
return;
}
try {
authRef.current?.untrackAll();
await socket.closeAllChannels();
console.log('Gracefully closed all channels.');
} catch (err) {
Expand All @@ -159,6 +217,7 @@ export const SocketClusterProvider = ({ children }) => {
return;
}
try {
authRef.current?.untrackAll();
await socket.killAllChannels();
console.log('Forcefully killed all channels.');
} catch (err) {
Expand All @@ -177,6 +236,7 @@ export const SocketClusterProvider = ({ children }) => {
killChannel,
closeAllChannels,
killAllChannels,
authenticateWithCheckoutToken,
}}
>
{children}
Expand Down
15 changes: 12 additions & 3 deletions src/hooks/use-qpay-checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function useQPayCheckout({ onOrderComplete }) {
const { t } = useLanguage();
const { customer, updateCustomerMeta } = useAuth();
const { currentLocation: deliveryLocation, updateDefaultLocation } = useCurrentLocation();
const { listen } = useSocketClusterClient();
const { listen, authenticateWithCheckoutToken } = useSocketClusterClient();
const [cart, updateCart] = useCart();
const isCheckingStatus = useRef(false);
const [checkoutOptions, setCheckoutOptions] = useState({
Expand All @@ -38,6 +38,9 @@ export default function useQPayCheckout({ onOrderComplete }) {
const [invoice, setInvoice] = useState();
const [checkoutId, setCheckoutId] = useState();
const [checkoutToken, setCheckoutToken] = useState();
// Checkout-scoped socket token (guests); kept in memory only.
const checkoutSocketTokenRef = useRef(null);
const isGuest = !customer;
const [serviceQuote, setServiceQuote] = useState(null);
const [isServiceQuoteUnavailable, setIsServiceQuoteUnavailable] = useState(false);
const [isLoading, setIsLoading] = useState(true);
Expand Down Expand Up @@ -178,7 +181,8 @@ export default function useQPayCheckout({ onOrderComplete }) {

setIsLoading(true);
try {
const { token, checkout, invoice } = await storefront.checkout.initialize(customer, cart, serviceQuote, 'qpay', checkoutOptions);
const { token, checkout, invoice, socket_token } = await storefront.checkout.initialize(customer, cart, serviceQuote, 'qpay', checkoutOptions);
checkoutSocketTokenRef.current = socket_token ?? null;
setInvoice(invoice);
setCheckoutId(checkout);
setCheckoutToken(token);
Expand Down Expand Up @@ -311,6 +315,11 @@ export default function useQPayCheckout({ onOrderComplete }) {
if (!checkoutId || !checkoutToken || listenerRef.current) return;

const listenForOrderStatus = async () => {
// Guests: authenticate with the checkout-scoped socket token before subscribing.
// Logged-in customers' own socket tokens already cover their checkouts.
if (isGuest && checkoutSocketTokenRef.current && typeof authenticateWithCheckoutToken === 'function') {
await authenticateWithCheckoutToken(checkoutSocketTokenRef.current);
}
console.log(`[Listener created for socket channel: checkout.${checkoutId}]`);
const listener = await listen(`checkout.${checkoutId}`, (event) => {
console.log(`[checkout channel ${checkoutId} event]`, event);
Expand All @@ -336,7 +345,7 @@ export default function useQPayCheckout({ onOrderComplete }) {
listenerRef.current = null;
}
};
}, [listen, checkoutId, checkoutToken, handleOrderCompletion, handlePaymentError]);
}, [listen, authenticateWithCheckoutToken, isGuest, checkoutId, checkoutToken, handleOrderCompletion, handlePaymentError]);

// Run order status check when the screen gains focus
useFocusEffect(
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/use-socket-cluster-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { consumeAsyncIterator, isAsyncIterable } from '../utils';
* Provides functionalities to subscribe, listen, and manage channels.
*/
const useSocketClusterClient = () => {
const { socket, isConnected, error, subscribeChannel, closeChannel, killChannel, closeAllChannels, killAllChannels } = useSocketCluster();
const { socket, isConnected, error, subscribeChannel, closeChannel, killChannel, closeAllChannels, killAllChannels, authenticateWithCheckoutToken } = useSocketCluster();

/**
* Listens to a channel for all incoming events/data.
Expand Down Expand Up @@ -70,6 +70,7 @@ const useSocketClusterClient = () => {
listen,
closeAllChannels,
killAllChannels,
authenticateWithCheckoutToken,
};
};

Expand Down
Loading
Loading