UNPKG

@oxyhq/services

Version:

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

517 lines (482 loc) • 19.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = _interopRequireWildcard(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 _bottomSheet = require("./bottomSheet"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } // 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, ...bottomSheetProps } = props; // Create internal bottom sheet ref const internalBottomSheetRef = (0, _react.useRef)(null); // If contextOnly is true, we just provide the context without the bottom sheet UI if (contextOnly) { return /*#__PURE__*/(0, _jsxRuntime.jsx)(_OxyContext.OxyContextProvider, { oxyServices: oxyServices, storageKeyPrefix: storageKeyPrefix, onAuthStateChange: onAuthStateChange, children: children }); } // Otherwise, provide both the context and the bottom sheet UI return /*#__PURE__*/(0, _jsxRuntime.jsx)(_OxyContext.OxyContextProvider, { oxyServices: oxyServices, 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, bottomSheetRef: internalBottomSheetRef, oxyServices: oxyServices }), children] })] }), !showInternalToaster && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.toasterContainer, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sonner.Toaster, { position: "top-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 = ({ oxyServices, initialScreen = 'SignIn', onClose, onAuthenticated, theme = 'light', customStyles = {}, bottomSheetRef, autoPresent = false, showInternalToaster = true }) => { // Use the internal ref (which is passed as a prop from OxyProvider) const modalRef = (0, _react.useRef)(null); // Create a ref to store the navigation function from OxyRouter const navigationRef = (0, _react.useRef)(null); // Track content height for dynamic sizing const [contentHeight, setContentHeight] = (0, _react.useState)(0); const [containerWidth, setContainerWidth] = (0, _react.useState)(800); // Track actual container width const screenHeight = _reactNative.Dimensions.get('window').height; // Set up effect to sync the internal ref with our modal ref (0, _react.useEffect)(() => { if (bottomSheetRef && modalRef.current) { // We need to expose certain methods to the internal ref const methodsToExpose = ['snapToIndex', 'snapToPosition', 'close', 'expand', 'collapse', 'present', 'dismiss']; methodsToExpose.forEach(method => { if (modalRef.current && typeof modalRef.current[method] === 'function') { // Properly forward methods from modalRef to bottomSheetRef // @ts-ignore - We're doing a runtime compatibility layer bottomSheetRef.current = bottomSheetRef.current || {}; // @ts-ignore - Dynamic method assignment bottomSheetRef.current[method] = (...args) => { // @ts-ignore - Dynamic method call return modalRef.current?.[method]?.(...args); }; } }); // Add a method to navigate between screens // @ts-ignore - Adding custom method bottomSheetRef.current._navigateToScreen = (screenName, props) => { console.log(`Navigation requested: ${screenName}`, props); // Try direct navigation function first (most reliable) if (navigationRef.current) { console.log('Using direct navigation function'); navigationRef.current(screenName, props); return; } // Fallback to event-based navigation if (typeof document !== 'undefined') { // For web - use a custom event console.log('Using web event navigation'); const event = new CustomEvent('oxy:navigate', { detail: { screen: screenName, props } }); document.dispatchEvent(event); } else { // For React Native - use the global variable approach console.log('Using React Native global navigation'); globalThis.oxyNavigateEvent = { screen: screenName, props }; } }; } }, [bottomSheetRef, modalRef]); // Use percentage-based snap points for better cross-platform compatibility const [snapPoints, setSnapPoints] = (0, _react.useState)(['60%', '90%']); // Animation values - we'll use these for content animations // Start with opacity 1 on Android to avoid visibility issues 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; const handleScaleAnim = (0, _react.useRef)(new _reactNative.Animated.Value(1)).current; // Track keyboard status const [keyboardVisible, setKeyboardVisible] = (0, _react.useState)(false); const [keyboardHeight, setKeyboardHeight] = (0, _react.useState)(0); const insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)(); // Get the authentication context const oxyContext = (0, _OxyContext.useOxy)(); // Handle keyboard events (0, _react.useEffect)(() => { const keyboardWillShowListener = _reactNative.Keyboard.addListener(_reactNative.Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', event => { // Debounce rapid keyboard events if (!keyboardVisible) { setKeyboardVisible(true); // Get keyboard height from event const keyboardHeightValue = event.endCoordinates.height; setKeyboardHeight(keyboardHeightValue); // Ensure the bottom sheet remains visible when keyboard opens // by adjusting to the highest snap point if (modalRef.current) { // Use requestAnimationFrame to avoid conflicts requestAnimationFrame(() => { modalRef.current?.snapToIndex(1); }); } } }); const keyboardWillHideListener = _reactNative.Keyboard.addListener(_reactNative.Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', () => { if (keyboardVisible) { setKeyboardVisible(false); setKeyboardHeight(0); } }); // Cleanup listeners return () => { keyboardWillShowListener.remove(); keyboardWillHideListener.remove(); }; }, [keyboardVisible]); // Present the modal when component mounts, but only if autoPresent is true (0, _react.useEffect)(() => { // Add expand method that handles presentation and animations if (bottomSheetRef && modalRef.current) { // Override expand to handle initial presentation // @ts-ignore - Dynamic method assignment bottomSheetRef.current.expand = () => { // Only present if not already presented modalRef.current?.present(); // Start content animations after presenting _reactNative.Animated.parallel([_reactNative.Animated.timing(fadeAnim, { toValue: 1, duration: 300, useNativeDriver: _reactNative.Platform.OS === 'ios' // Only use native driver on iOS }), _reactNative.Animated.spring(slideAnim, { toValue: 0, friction: 8, tension: 40, useNativeDriver: _reactNative.Platform.OS === 'ios' // Only use native driver on iOS })]).start(); }; } // Auto-present if the autoPresent prop is true if (autoPresent && modalRef.current) { // Small delay to allow everything to initialize const timer = setTimeout(() => { modalRef.current?.present(); // Start content animations after presenting _reactNative.Animated.parallel([_reactNative.Animated.timing(fadeAnim, { toValue: 1, duration: 300, useNativeDriver: _reactNative.Platform.OS === 'ios' // Only use native driver on iOS }), _reactNative.Animated.spring(slideAnim, { toValue: 0, friction: 8, tension: 40, useNativeDriver: _reactNative.Platform.OS === 'ios' // Only use native driver on iOS })]).start(); }, 100); return () => clearTimeout(timer); } }, [bottomSheetRef, modalRef, fadeAnim, slideAnim, autoPresent]); // Handle authentication success from the bottom sheet screens const handleAuthenticated = (0, _react.useCallback)(user => { // Call the prop callback if provided if (onAuthenticated) { onAuthenticated(user); } }, [onAuthenticated]); // Handle indicator animation - subtle pulse effect (0, _react.useEffect)(() => { const pulseAnimation = _reactNative.Animated.sequence([_reactNative.Animated.timing(handleScaleAnim, { toValue: 1.1, duration: 300, useNativeDriver: _reactNative.Platform.OS === 'ios' // Only use native driver on iOS }), _reactNative.Animated.timing(handleScaleAnim, { toValue: 1, duration: 300, useNativeDriver: _reactNative.Platform.OS === 'ios' // Only use native driver on iOS })]); // Run the animation once when component mounts pulseAnimation.start(); }, []); // Handle backdrop rendering const renderBackdrop = (0, _react.useCallback)(props => /*#__PURE__*/(0, _jsxRuntime.jsx)(_bottomSheet.BottomSheetBackdrop, { ...props, disappearsOnIndex: -1, appearsOnIndex: 0, opacity: 0.5 }), []); // Background style based on theme const getBackgroundStyle = () => { const baseColor = customStyles.backgroundColor || (theme === 'light' ? '#FFFFFF' : '#121212'); return { backgroundColor: baseColor, // Make sure there's no transparency opacity: 1, // Additional Android-specific styles ..._reactNative.Platform.select({ android: { elevation: 24 } }) }; }; // Method to adjust snap points from Router const adjustSnapPoints = (0, _react.useCallback)(points => { // Ensure snap points are high enough when keyboard is visible if (keyboardVisible) { // If keyboard is visible, make sure we use higher snap points // to ensure the sheet content remains visible const highestPoint = points[points.length - 1]; setSnapPoints([highestPoint, highestPoint]); } else { // If we have content height, use it as a constraint if (contentHeight > 0) { // Calculate content height as percentage of screen (plus some padding) // Clamp to ensure we don't exceed 90% to leave space for UI elements const contentHeightPercent = Math.min(Math.ceil(contentHeight / screenHeight * 100), 90); const contentHeightPercentStr = `${contentHeightPercent}%`; // Use content height for first snap point if it's taller than the default const firstPoint = contentHeight / screenHeight > 0.6 ? contentHeightPercentStr : points[0]; setSnapPoints([firstPoint, points[1] || '90%']); } else { setSnapPoints(points); } } }, [keyboardVisible, contentHeight, screenHeight]); // Handle content layout changes to measure height and width const handleContentLayout = (0, _react.useCallback)(event => { const { height: layoutHeight, width: layoutWidth } = event.nativeEvent.layout; setContentHeight(layoutHeight); setContainerWidth(layoutWidth); // Debug: log container dimensions console.log('[OxyProvider] Container dimensions (full):', { width: layoutWidth, height: layoutHeight }); // Update snap points based on new content height if (keyboardVisible) { // If keyboard is visible, use the highest snap point const highestPoint = snapPoints[snapPoints.length - 1]; setSnapPoints([highestPoint, highestPoint]); } else { if (layoutHeight > 0) { // Add padding and clamp to reasonable limits const contentHeightPercent = Math.min(Math.ceil((layoutHeight + 40) / screenHeight * 100), 90); const contentHeightPercentStr = `${contentHeightPercent}%`; const firstPoint = layoutHeight / screenHeight > 0.6 ? contentHeightPercentStr : snapPoints[0]; setSnapPoints([firstPoint, snapPoints[1]]); } } }, [keyboardVisible, screenHeight, snapPoints]); // Close the bottom sheet with animation const handleClose = (0, _react.useCallback)(() => { // Animate content out _reactNative.Animated.timing(fadeAnim, { toValue: 0, duration: _reactNative.Platform.OS === 'android' ? 100 : 200, // Faster on Android useNativeDriver: _reactNative.Platform.OS === 'ios' // Only use native driver on iOS }).start(() => { // Dismiss the sheet modalRef.current?.dismiss(); if (onClose) { setTimeout(() => { onClose(); }, _reactNative.Platform.OS === 'android' ? 150 : 100); } }); }, [onClose, fadeAnim]); // Handle sheet index changes const handleSheetChanges = (0, _react.useCallback)(index => {}, [onClose, handleScaleAnim, keyboardVisible]); return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_bottomSheet.BottomSheetModal, { ref: modalRef, index: 0, snapPoints: snapPoints, enablePanDownToClose: true, backdropComponent: renderBackdrop, backgroundStyle: [getBackgroundStyle(), { borderTopLeftRadius: 35, borderTopRightRadius: 35 }], handleIndicatorStyle: { backgroundColor: customStyles.handleColor || (theme === 'light' ? '#CCCCCC' : '#444444'), width: 40, height: 4 }, onChange: handleSheetChanges, style: styles.bottomSheetContainer // Adding additional props to improve layout behavior , keyboardBehavior: "interactive", keyboardBlurBehavior: "restore", android_keyboardInputMode: "adjustResize", enableOverDrag: true, enableContentPanningGesture: true, enableHandlePanningGesture: true, overDragResistanceFactor: 2.5, enableBlurKeyboardOnGesture: true // Log sheet animations for debugging , onAnimate: (fromIndex, toIndex) => { console.log(`Animating from index ${fromIndex} to ${toIndex}`); }, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_bottomSheet.BottomSheetScrollView, { style: [styles.contentContainer, // Override padding if provided in customStyles customStyles.contentPadding !== undefined && { padding: customStyles.contentPadding }], onLayout: handleContentLayout, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.centeredContentWrapper, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, { style: [styles.animatedContent, // Apply animations - conditionally for Android _reactNative.Platform.OS === 'android' ? { opacity: 1 // No fade animation on Android } : { opacity: fadeAnim, transform: [{ translateY: slideAnim }] }], children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_OxyRouter.default, { oxyServices: oxyServices, initialScreen: initialScreen, onClose: handleClose, onAuthenticated: handleAuthenticated, theme: theme, adjustSnapPoints: adjustSnapPoints, navigationRef: navigationRef, containerWidth: containerWidth }) }) }) }), showInternalToaster && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.toasterContainer, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sonner.Toaster, { position: "top-center", swipeToDismissDirection: "left" }) })] }); }; const styles = _reactNative.StyleSheet.create({ bottomSheetContainer: { maxWidth: 800, width: '100%', marginHorizontal: 'auto' }, contentContainer: { width: '100%', borderTopLeftRadius: 35, borderTopRightRadius: 35 }, centeredContentWrapper: { width: '100%', marginHorizontal: 'auto' }, 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