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