UNPKG

@oxyhq/services

Version:

Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀

447 lines (432 loc) • 15.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = require("react"); var _reactNative = require("react-native"); var _reactNativeSafeAreaContext = require("react-native-safe-area-context"); var _reactNativeGestureHandler = require("react-native-gesture-handler"); var _OxyContext = require("../context/OxyContext"); var _OxyRouter = _interopRequireDefault(require("../navigation/OxyRouter")); var _FontLoader = require("./FontLoader"); var _sonner = require("../../lib/sonner"); var _reactQuery = require("@tanstack/react-query"); var _bottomSheet = require("@gorhom/bottom-sheet"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } // Import bottom sheet components directly - no longer a peer dependency // Initialize fonts automatically (0, _FontLoader.setupFonts)(); /** * Enhanced OxyProvider component * * This component serves two purposes: * 1. As a context provider for authentication and session management across the app * 2. As a UI component for authentication and account management using a bottom sheet */ const OxyProvider = props => { const { oxyServices, children, contextOnly = false, onAuthStateChange, storageKeyPrefix, showInternalToaster = true, baseURL, // Add support for baseURL ...bottomSheetProps } = props; // Create typed internal bottom sheet controller ref const internalBottomSheetRef = (0, _react.useRef)(null); // Initialize React Query Client (use provided client or create a default one once) const queryClientRef = (0, _react.useRef)(null); if (!queryClientRef.current) { queryClientRef.current = props.queryClient ?? new _reactQuery.QueryClient({ defaultOptions: { queries: { staleTime: 30_000, gcTime: 5 * 60_000, retry: 2, refetchOnReconnect: true, refetchOnWindowFocus: false }, mutations: { retry: 1 } } }); } // Hook React Query focus manager into React Native AppState (0, _react.useEffect)(() => { const subscription = _reactNative.AppState.addEventListener('change', state => { _reactQuery.focusManager.setFocused(state === 'active'); }); return () => { subscription.remove(); }; }, []); // Mirror internal controller to external ref if provided (back-compat) (0, _react.useEffect)(() => { if (props.bottomSheetRef) { props.bottomSheetRef.current = internalBottomSheetRef.current; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.bottomSheetRef]); // If contextOnly is true, we just provide the context without the bottom sheet UI if (contextOnly) { return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactQuery.QueryClientProvider, { client: queryClientRef.current, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_OxyContext.OxyContextProvider, { oxyServices: oxyServices, baseURL: baseURL, storageKeyPrefix: storageKeyPrefix, onAuthStateChange: onAuthStateChange, children: children }) }); } // Otherwise, provide both the context and the bottom sheet UI return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactQuery.QueryClientProvider, { client: queryClientRef.current, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_OxyContext.OxyContextProvider, { oxyServices: oxyServices, baseURL: baseURL, storageKeyPrefix: storageKeyPrefix, onAuthStateChange: onAuthStateChange, bottomSheetRef: internalBottomSheetRef, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_FontLoader.FontLoader, { children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeGestureHandler.GestureHandlerRootView, { style: styles.gestureHandlerRoot, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_bottomSheet.BottomSheetModalProvider, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.StatusBar, { translucent: true, backgroundColor: "transparent" }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeSafeAreaContext.SafeAreaProvider, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(OxyBottomSheet, { ...bottomSheetProps, ref: internalBottomSheetRef, oxyServices: oxyServices }), children] })] }), !showInternalToaster && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.toasterContainer, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sonner.Toaster, { position: "bottom-center", swipeToDismissDirection: "left", offset: 15 }) })] }) }) }) }); }; /** * OxyBottomSheet component - A bottom sheet-based authentication and account management UI * * This is the original OxyProvider UI functionality, now extracted into its own component * and reimplemented using BottomSheetModal for better Android compatibility */ const OxyBottomSheet = /*#__PURE__*/(0, _react.forwardRef)(({ oxyServices: providedOxyServices, initialScreen = 'SignIn', onClose, onAuthenticated, theme = 'light', customStyles = {}, autoPresent = false, showInternalToaster = true, appInsets }, ref) => { // Helper function to determine if native driver should be used const shouldUseNativeDriver = () => { return _reactNative.Platform.OS === 'ios'; }; // Get window dimensions for max height calculation const { height: windowHeight } = (0, _reactNative.useWindowDimensions)(); // Get oxyServices from context if not provided as prop const contextOxy = (0, _OxyContext.useOxy)(); const oxyServices = providedOxyServices || contextOxy?.oxyServices; // Use the internal ref (which is passed as a prop from OxyProvider) const modalRef = (0, _react.useRef)(null); const isOpenRef = (0, _react.useRef)(false); const navigationRef = (0, _react.useRef)(null); // Remove contentHeight, containerWidth, and snap point state/logic // Animation values - keep for content fade/slide const fadeAnim = (0, _react.useRef)(new _reactNative.Animated.Value(_reactNative.Platform.OS === 'android' ? 1 : 0)).current; const slideAnim = (0, _react.useRef)(new _reactNative.Animated.Value(_reactNative.Platform.OS === 'android' ? 0 : 50)).current; // Expose a clean, typed imperative API (0, _react.useImperativeHandle)(ref, () => ({ present: () => { if (!isOpenRef.current) modalRef.current?.present?.(); }, dismiss: () => modalRef.current?.dismiss?.(), expand: () => { // Ensure presented, then animate content in if (!isOpenRef.current) modalRef.current?.present?.(); _reactNative.Animated.parallel([_reactNative.Animated.timing(fadeAnim, { toValue: 1, duration: 300, useNativeDriver: shouldUseNativeDriver() }), _reactNative.Animated.spring(slideAnim, { toValue: 0, friction: 8, tension: 40, useNativeDriver: shouldUseNativeDriver() })]).start(); }, collapse: () => modalRef.current?.collapse?.(), snapToIndex: index => modalRef.current?.snapToIndex?.(index), snapToPosition: position => modalRef.current?.snapToPosition?.(position), navigate: (screen, props) => { if (navigationRef.current) { navigationRef.current(screen, props); return; } if (typeof document !== 'undefined') { const event = new CustomEvent('oxy:navigate', { detail: { screen, props } }); document.dispatchEvent(event); } else { globalThis.oxyNavigateEvent = { screen, props }; } } }), [fadeAnim, slideAnim]); const insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)(); // Track keyboard state for dynamic padding (not for bottomInset - library handles that) const [keyboardHeight, setKeyboardHeight] = (0, _react.useState)(0); (0, _react.useEffect)(() => { const showSubscription = _reactNative.Keyboard.addListener('keyboardDidShow', e => { setKeyboardHeight(e.endCoordinates.height); }); const hideSubscription = _reactNative.Keyboard.addListener('keyboardDidHide', () => { setKeyboardHeight(0); }); return () => { showSubscription.remove(); hideSubscription.remove(); }; }, []); // Calculate max height for dynamic sizing (screen height minus insets and margin) // Note: keyboardBehavior="interactive" handles keyboard positioning automatically const maxHeight = (0, _react.useMemo)(() => { const topInset = (insets?.top ?? 0) + (appInsets?.top ?? 0); const bottomInset = (insets?.bottom ?? 0) + (appInsets?.bottom ?? 0); return windowHeight - topInset - bottomInset - 20; // 20px margin }, [windowHeight, insets?.top, insets?.bottom, appInsets?.top, appInsets?.bottom]); // Present the modal when component mounts, but only if autoPresent is true (0, _react.useEffect)(() => { if (autoPresent && modalRef.current) { const timer = setTimeout(() => { modalRef.current?.present(); _reactNative.Animated.parallel([_reactNative.Animated.timing(fadeAnim, { toValue: 1, duration: 300, useNativeDriver: shouldUseNativeDriver() }), _reactNative.Animated.spring(slideAnim, { toValue: 0, friction: 8, tension: 40, useNativeDriver: shouldUseNativeDriver() })]).start(); }, 100); return () => clearTimeout(timer); } }, [modalRef, autoPresent]); // Close the bottom sheet with animation (unchanged) const handleClose = (0, _react.useCallback)(() => { _reactNative.Animated.timing(fadeAnim, { toValue: 0, duration: _reactNative.Platform.OS === 'android' ? 100 : 200, useNativeDriver: shouldUseNativeDriver() }).start(() => { modalRef.current?.dismiss(); if (onClose) { setTimeout(() => { onClose(); }, _reactNative.Platform.OS === 'android' ? 150 : 100); } }); }, [onClose]); // Handle authentication success (unchanged) const handleAuthenticated = (0, _react.useCallback)(user => { fadeAnim.stopAnimation(); slideAnim.stopAnimation(); if (onAuthenticated) { onAuthenticated(user); } modalRef.current?.dismiss(); if (onClose) { setTimeout(() => { onClose(); }, 100); } }, [onAuthenticated, onClose]); // Backdrop rendering (unchanged) const renderBackdrop = (0, _react.useCallback)(props => /*#__PURE__*/(0, _jsxRuntime.jsx)(_bottomSheet.BottomSheetBackdrop, { ...props, disappearsOnIndex: -1, appearsOnIndex: 0, opacity: 0.5 }), []); // Modernized BottomSheetModal usage return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_bottomSheet.BottomSheetModal, { ref: modalRef, index: 0, enableDynamicSizing: true, maxDynamicContentSize: maxHeight, enablePanDownToClose: true, backdropComponent: renderBackdrop, backgroundStyle: [{ borderBottomLeftRadius: 0, borderBottomRightRadius: 0, borderTopLeftRadius: 35, borderTopRightRadius: 35 }], handleIndicatorStyle: { backgroundColor: customStyles.handleColor || (theme === 'light' ? '#CCCCCC' : '#444444'), width: 40, height: 4 }, handleStyle: { position: 'absolute', top: 0, left: 0, right: 0 }, style: styles.bottomSheetContainer, keyboardBehavior: _reactNative.Platform.OS === 'ios' ? 'extend' : 'interactive', keyboardBlurBehavior: "restore", android_keyboardInputMode: "adjustResize", enableOverDrag: false, enableContentPanningGesture: true, enableHandlePanningGesture: true, overDragResistanceFactor: 2.5, enableBlurKeyboardOnGesture: true, detached: true, topInset: (insets?.top ?? 0) + (appInsets?.top ?? 0), bottomInset: (insets?.bottom ?? 0) + (appInsets?.bottom ?? 0), onChange: index => { isOpenRef.current = index !== -1; }, onDismiss: () => { isOpenRef.current = false; }, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_bottomSheet.BottomSheetScrollView, { style: [styles.contentContainer], contentContainerStyle: [styles.scrollContentContainer, { paddingBottom: keyboardHeight > 0 ? keyboardHeight : 0 }], showsVerticalScrollIndicator: true, bounces: false, nestedScrollEnabled: true, keyboardShouldPersistTaps: "handled", keyboardDismissMode: "on-drag", children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, { style: [styles.animatedContent, _reactNative.Platform.OS === 'android' ? { opacity: 1 } : { opacity: fadeAnim, transform: [{ translateY: slideAnim }] }], children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.centeredContentWrapper, { paddingBottom: (insets?.bottom ?? 0) + (appInsets?.bottom ?? 0) }], children: oxyServices ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_OxyRouter.default, { oxyServices: oxyServices, initialScreen: initialScreen, onClose: handleClose, onAuthenticated: handleAuthenticated, theme: theme, navigationRef: navigationRef, containerWidth: 800 // static, since dynamic sizing is used }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.errorContainer, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { children: "OxyServices not available" }) }) }) }) }), showInternalToaster && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.toasterContainer, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sonner.Toaster, { position: "bottom-center", swipeToDismissDirection: "left" }) })] }); }); const styles = _reactNative.StyleSheet.create({ bottomSheetContainer: { maxWidth: 800, width: '100%', alignSelf: 'center', marginHorizontal: 'auto' }, contentContainer: { width: '100%', borderTopLeftRadius: 35, borderTopRightRadius: 35 }, scrollContentContainer: { // Content will size naturally, ScrollView handles overflow // paddingBottom is set dynamically based on keyboard height }, centeredContentWrapper: { width: '100%', alignSelf: 'center' }, animatedContent: { width: '100%' }, indicator: { width: 40, height: 4, marginTop: 8, marginBottom: 8, borderRadius: 35 }, errorContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' }, gestureHandlerRoot: { flex: 1, position: 'relative', backgroundColor: 'transparent', ..._reactNative.Platform.select({ android: { height: '100%', width: '100%' } }) }, toasterContainer: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 9999, elevation: 9999, // For Android pointerEvents: 'box-none' // Allow touches to pass through to underlying components } }); var _default = exports.default = OxyProvider; //# sourceMappingURL=OxyProvider.js.map