@oxyhq/services
Version:
552 lines (533 loc) • 18.7 kB
JavaScript
"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