@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
JavaScript
"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