UNPKG

@oxyhq/services

Version:

OxyHQ Expo/React Native SDK — UI components, screens, and native features

552 lines (533 loc) 18.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.subscribeToSignInModal = exports.showSignInModal = exports.isSignInModalVisible = exports.hideSignInModal = exports.default = void 0; var _react = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated")); var _reactNativeSafeAreaContext = require("react-native-safe-area-context"); var _socket = _interopRequireDefault(require("socket.io-client")); var _reactNativeQrcodeSvg = _interopRequireDefault(require("react-native-qrcode-svg")); var _useThemeColors = require("../hooks/useThemeColors.js"); var _OxyContext = require("../context/OxyContext.js"); var _OxyLogo = _interopRequireDefault(require("./OxyLogo.js")); var _core = require("@oxyhq/core"); 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); } /** * SignInModal - Full screen sign-in modal with QR code * * A semi-transparent full-screen modal that displays: * - QR code for scanning with Oxy Accounts app * - Button to open Oxy Auth popup * * Animates with fade-in effect. */ const debug = (0, _core.createDebugLogger)('SignInModal'); const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = _reactNative.Dimensions.get('window'); // Auth session expiration (5 minutes) const AUTH_SESSION_EXPIRY_MS = 5 * 60 * 1000; // Polling interval (fallback if socket fails) const POLLING_INTERVAL_MS = 3000; // Store for modal visibility with subscription support let modalVisible = false; let setModalVisibleCallback = null; const visibilityListeners = new Set(); const showSignInModal = () => { modalVisible = true; setModalVisibleCallback?.(true); visibilityListeners.forEach(listener => listener(true)); }; exports.showSignInModal = showSignInModal; const hideSignInModal = () => { modalVisible = false; setModalVisibleCallback?.(false); visibilityListeners.forEach(listener => listener(false)); }; exports.hideSignInModal = hideSignInModal; const isSignInModalVisible = () => modalVisible; /** Subscribe to modal visibility changes */ exports.isSignInModalVisible = isSignInModalVisible; const subscribeToSignInModal = listener => { visibilityListeners.add(listener); return () => visibilityListeners.delete(listener); }; exports.subscribeToSignInModal = subscribeToSignInModal; const SignInModal = () => { const [visible, setVisible] = (0, _react.useState)(false); const [authSession, setAuthSession] = (0, _react.useState)(null); const [isLoading, setIsLoading] = (0, _react.useState)(false); const [error, setError] = (0, _react.useState)(null); const [isWaiting, setIsWaiting] = (0, _react.useState)(false); const insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)(); const colors = (0, _useThemeColors.useThemeColors)(); const { oxyServices, switchSession } = (0, _OxyContext.useOxy)(); const socketRef = (0, _react.useRef)(null); const pollingIntervalRef = (0, _react.useRef)(null); const isProcessingRef = (0, _react.useRef)(false); // Animation values const opacity = (0, _reactNativeReanimated.useSharedValue)(0); const scale = (0, _reactNativeReanimated.useSharedValue)(0.9); // Register callback (0, _react.useEffect)(() => { setModalVisibleCallback = setVisible; return () => { setModalVisibleCallback = null; }; }, []); // Animate in/out (0, _react.useEffect)(() => { if (visible) { opacity.value = (0, _reactNativeReanimated.withTiming)(1, { duration: 250 }); scale.value = (0, _reactNativeReanimated.withTiming)(1, { duration: 250 }); generateAuthSession(); } else { opacity.value = (0, _reactNativeReanimated.withTiming)(0, { duration: 200 }); scale.value = (0, _reactNativeReanimated.withTiming)(0.9, { duration: 200 }); } }, [visible]); const backdropStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({ opacity: opacity.value })); const contentStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({ opacity: opacity.value, transform: [{ scale: scale.value }] })); // Handle successful authorization const handleAuthSuccess = (0, _react.useCallback)(async sessionId => { if (isProcessingRef.current) return; isProcessingRef.current = true; try { if (switchSession) { await switchSession(sessionId); } else { await oxyServices.getTokenBySession(sessionId); } hideSignInModal(); } catch (err) { debug.error('Error completing auth:', err); setError('Authorization successful but failed to complete sign in. Please try again.'); isProcessingRef.current = false; } }, [oxyServices, switchSession]); // Cleanup socket and polling const cleanup = (0, _react.useCallback)(() => { setIsWaiting(false); if (socketRef.current) { socketRef.current.disconnect(); socketRef.current = null; } if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } }, []); // Connect to socket for real-time updates const connectSocket = (0, _react.useCallback)(sessionToken => { const baseURL = oxyServices.getBaseURL(); const socket = (0, _socket.default)(`${baseURL}/auth-session`, { transports: ['websocket', 'polling'], reconnection: true, reconnectionAttempts: 3, reconnectionDelay: 1000 }); socketRef.current = socket; socket.on('connect', () => { debug.log('Auth socket connected'); socket.emit('join', sessionToken); }); socket.on('auth_update', payload => { debug.log('Auth update received:', payload); if (payload.status === 'authorized' && payload.sessionId) { cleanup(); handleAuthSuccess(payload.sessionId); } else if (payload.status === 'cancelled') { cleanup(); setError('Authorization was denied.'); } else if (payload.status === 'expired') { cleanup(); setError('Session expired. Please try again.'); } }); socket.on('connect_error', err => { debug.log('Socket connection error, falling back to polling:', err.message); socket.disconnect(); startPolling(sessionToken); }); }, [oxyServices, handleAuthSuccess, cleanup]); // Start polling for authorization (fallback) const startPolling = (0, _react.useCallback)(sessionToken => { pollingIntervalRef.current = setInterval(async () => { if (isProcessingRef.current) return; try { const response = await oxyServices.makeRequest('GET', `/auth/session/status/${sessionToken}`, undefined, { cache: false }); if (response.authorized && response.sessionId) { cleanup(); handleAuthSuccess(response.sessionId); } else if (response.status === 'cancelled') { cleanup(); setError('Authorization was denied.'); } else if (response.status === 'expired') { cleanup(); setError('Session expired. Please try again.'); } } catch (err) { debug.log('Auth polling error:', err); } }, POLLING_INTERVAL_MS); }, [oxyServices, handleAuthSuccess, cleanup]); // Generate a new auth session const generateAuthSession = (0, _react.useCallback)(async () => { setIsLoading(true); setError(null); isProcessingRef.current = false; try { const sessionToken = generateSessionToken(); const expiresAt = Date.now() + AUTH_SESSION_EXPIRY_MS; await oxyServices.makeRequest('POST', '/auth/session/create', { sessionToken, expiresAt, appId: _reactNative.Platform.OS }, { cache: false }); setAuthSession({ sessionToken, expiresAt }); setIsWaiting(true); connectSocket(sessionToken); } catch (err) { setError(err.message || 'Failed to create auth session'); } finally { setIsLoading(false); } }, [oxyServices, connectSocket]); // Generate a random session token const generateSessionToken = () => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < 32; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; }; // Build the QR code data const getQRData = () => { if (!authSession) return ''; return `oxyauth://${authSession.sessionToken}`; }; // Open Oxy Auth popup const handleOpenAuthPopup = (0, _react.useCallback)(async () => { if (!authSession) return; const baseURL = oxyServices.getBaseURL(); // Resolve auth web URL let authWebUrl = oxyServices.config?.authWebUrl; if (!authWebUrl) { try { const url = new URL(baseURL); if (url.port === '3001') { url.port = '3000'; authWebUrl = url.origin; } else if (url.hostname.startsWith('api.')) { url.hostname = `auth.${url.hostname.slice(4)}`; authWebUrl = url.origin; } } catch { authWebUrl = 'https://auth.oxy.so'; } } authWebUrl = authWebUrl || 'https://auth.oxy.so'; const webUrl = new URL('/authorize', authWebUrl); webUrl.searchParams.set('token', authSession.sessionToken); if (_reactNative.Platform.OS === 'web') { // Open popup window on web const width = 500; const height = 650; const screenWidth = window.screen?.width ?? width; const screenHeight = window.screen?.height ?? height; const left = (screenWidth - width) / 2; const top = (screenHeight - height) / 2; window.open(webUrl.toString(), 'oxy-auth-popup', `width=${width},height=${height},left=${left},top=${top},popup=1`); } else { // Open in browser on native _reactNative.Linking.openURL(webUrl.toString()); } }, [authSession, oxyServices]); // Refresh session const handleRefresh = (0, _react.useCallback)(() => { cleanup(); generateAuthSession(); }, [generateAuthSession, cleanup]); // Handle close const handleClose = (0, _react.useCallback)(() => { cleanup(); hideSignInModal(); }, [cleanup]); // Clean up on unmount (0, _react.useEffect)(() => { return () => { cleanup(); }; }, [cleanup]); if (!visible) return null; return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Modal, { visible: visible, transparent: true, animationType: "none", statusBarTranslucent: true, onRequestClose: handleClose, children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, { style: [styles.backdrop, backdropStyle], children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: _reactNative.StyleSheet.absoluteFill, onPress: handleClose, activeOpacity: 1 }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, { style: [styles.content, contentStyle, { paddingTop: insets.top + 20, paddingBottom: insets.bottom + 20 }], children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: styles.closeButton, onPress: handleClose, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: styles.closeButtonText, children: "\xD7" }) }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.header, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_OxyLogo.default, { width: 56, height: 56 }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.title, { color: colors.text }], children: "Sign in with Oxy" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.subtitle, { color: colors.secondaryText }], children: "Scan with Oxy Accounts app or use the button below" })] }), isLoading ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.loadingContainer, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "large", color: colors.tint }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.loadingText, { color: colors.secondaryText }], children: "Preparing sign in..." })] }) : error ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.errorContainer, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.errorText, { color: '#EA4335' }], children: error }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: [styles.button, { backgroundColor: colors.tint }], onPress: handleRefresh, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: styles.buttonText, children: "Try Again" }) })] }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.qrContainer, { backgroundColor: 'white' }], children: authSession ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeQrcodeSvg.default, { value: getQRData(), size: 200, backgroundColor: "white", color: "black" }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "large", color: "#d169e5" }) }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.dividerContainer, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.divider, { backgroundColor: 'rgba(255,255,255,0.3)' }] }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.dividerText, { color: 'rgba(255,255,255,0.7)' }], children: "or" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.divider, { backgroundColor: 'rgba(255,255,255,0.3)' }] })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, { style: [styles.button, { backgroundColor: '#d169e5' }], onPress: handleOpenAuthPopup, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_OxyLogo.default, { width: 20, height: 20, fillColor: "white", style: styles.buttonIcon }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: styles.buttonText, children: "Open Oxy Auth" })] }), isWaiting && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.statusContainer, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "small", color: "white" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: styles.statusText, children: "Waiting for authorization..." })] })] })] })] }) }); }; const styles = _reactNative.StyleSheet.create({ backdrop: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.85)', justifyContent: 'center', alignItems: 'center' }, content: { width: '100%', maxWidth: 400, alignItems: 'center', paddingHorizontal: 24 }, closeButton: { position: 'absolute', top: 16, right: 16, width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255, 255, 255, 0.1)', justifyContent: 'center', alignItems: 'center', zIndex: 10 }, closeButtonText: { color: 'white', fontSize: 28, fontWeight: '300', lineHeight: 32 }, header: { alignItems: 'center', marginBottom: 32 }, title: { fontSize: 28, fontWeight: 'bold', marginTop: 16, color: 'white' }, subtitle: { fontSize: 15, marginTop: 8, textAlign: 'center', color: 'rgba(255, 255, 255, 0.7)' }, qrContainer: { padding: 20, borderRadius: 16 }, dividerContainer: { flexDirection: 'row', alignItems: 'center', marginVertical: 24, width: '100%' }, divider: { flex: 1, height: 1 }, dividerText: { marginHorizontal: 16, fontSize: 14 }, button: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 16, paddingHorizontal: 24, borderRadius: 12, width: '100%' }, buttonIcon: { marginRight: 10 }, buttonText: { color: 'white', fontSize: 16, fontWeight: '600' }, statusContainer: { flexDirection: 'row', alignItems: 'center', marginTop: 24 }, statusText: { marginLeft: 8, fontSize: 14, color: 'rgba(255, 255, 255, 0.7)' }, loadingContainer: { alignItems: 'center', paddingVertical: 40 }, loadingText: { marginTop: 16, fontSize: 14 }, errorContainer: { alignItems: 'center', paddingVertical: 20, width: '100%' }, errorText: { fontSize: 14, textAlign: 'center', marginBottom: 16 } }); var _default = exports.default = SignInModal; //# sourceMappingURL=SignInModal.js.map