UNPKG

@oxyhq/services

Version:

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

446 lines (437 loc) • 17.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated")); var _OxyContext = require("../context/OxyContext"); var _Avatar = _interopRequireDefault(require("../components/Avatar")); var _vectorIcons = require("@expo/vector-icons"); var _sonner = require("../../lib/sonner"); var _authStore = require("../stores/authStore"); var _styles = require("../styles"); var _GroupedPillButtons = _interopRequireDefault(require("../components/internal/GroupedPillButtons")); var _useI18n = require("../hooks/useI18n"); 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); } const GAP = 12; const INNER_GAP = 8; // Individual animated progress dot const AnimatedProgressDot = ({ isActive, colors, styles }) => { const width = (0, _reactNativeReanimated.useSharedValue)(isActive ? 12 : 6); const backgroundColor = (0, _reactNativeReanimated.useSharedValue)(isActive ? colors.primary : colors.border); (0, _react.useEffect)(() => { width.value = (0, _reactNativeReanimated.withTiming)(isActive ? 12 : 6, { duration: 300 }); backgroundColor.value = (0, _reactNativeReanimated.withTiming)(isActive ? colors.primary : colors.border, { duration: 300 }); }, [isActive, colors.primary, colors.border, width, backgroundColor]); const animatedStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({ width: width.value, backgroundColor: backgroundColor.value })); return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, { style: [styles.progressDot, animatedStyle] }); }; /** * Post-signup welcome & onboarding screen. * - Greets the newly registered user * - Lets them immediately set / change their avatar using existing FileManagement picker * - Only when the user presses "Continue" do we invoke onAuthenticated to finish flow & close sheet */ const WelcomeNewUserScreen = ({ navigate, onAuthenticated, theme, newUser }) => { const { user, oxyServices } = (0, _OxyContext.useOxy)(); const { t } = (0, _useI18n.useI18n)(); const updateUser = (0, _authStore.useAuthStore)(s => s.updateUser); const currentUser = user || newUser; // fallback const colors = (0, _styles.useThemeColors)(theme); const styles = (0, _react.useMemo)(() => createStyles(theme), [theme]); // Animation state const fadeAnim = (0, _react.useRef)(new _reactNative.Animated.Value(1)).current; const slideAnim = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current; const [currentStep, setCurrentStep] = (0, _react.useState)(0); // Track avatar separately to ensure it updates immediately after selection const [selectedAvatarId, setSelectedAvatarId] = (0, _react.useState)(currentUser?.avatar); // Update selectedAvatarId when user changes (0, _react.useEffect)(() => { if (user?.avatar) { setSelectedAvatarId(user.avatar); } else if (newUser?.avatar) { setSelectedAvatarId(newUser.avatar); } }, [user?.avatar, newUser?.avatar]); const avatarUri = selectedAvatarId ? oxyServices.getFileDownloadUrl(selectedAvatarId, 'thumb') : undefined; // Steps content const welcomeTitle = currentUser?.username ? t('welcomeNew.welcome.titleWithName', { username: currentUser.username }) || `Welcome, ${currentUser.username} 👋` : t('welcomeNew.welcome.title') || 'Welcome 👋'; const steps = [{ key: 'welcome', title: welcomeTitle, body: t('welcomeNew.welcome.body') || "You just created an account in a calm, ethical space. A few quick things — then you're in." }, { key: 'account', title: t('welcomeNew.account.title') || 'One Account. Simple.', bullets: [t('welcomeNew.account.bullets.0') || 'One identity across everything', t('welcomeNew.account.bullets.1') || 'No re‑signing in all the time', t('welcomeNew.account.bullets.2') || 'Your preferences stay with you'] }, { key: 'principles', title: t('welcomeNew.principles.title') || 'What We Stand For', bullets: [t('welcomeNew.principles.bullets.0') || 'Privacy by default', t('welcomeNew.principles.bullets.1') || 'No manipulative feeds', t('welcomeNew.principles.bullets.2') || 'You decide what to share', t('welcomeNew.principles.bullets.3') || 'No selling your data'] }, { key: 'karma', title: t('welcomeNew.karma.title') || 'Karma = Trust & Growth', body: t('welcomeNew.karma.body') || 'Oxy Karma is a points system that reacts to what you do. Helpful, respectful, constructive actions earn it. Harmful or low‑effort stuff chips it away. More karma can unlock benefits; low karma can limit features. It keeps things fair and rewards real contribution.' }, { key: 'avatar', title: t('welcomeNew.avatar.title') || 'Make It Yours', body: t('welcomeNew.avatar.body') || 'Add an avatar so people recognize you. It will show anywhere you show up here. Skip if you want — you can add it later.', showAvatar: true }, { key: 'ready', title: t('welcomeNew.ready.title') || "You're Ready", body: t('welcomeNew.ready.body') || 'Explore. Contribute. Earn karma. Stay in control.' }]; const totalSteps = steps.length; const avatarStepIndex = steps.findIndex(s => s.showAvatar); const animateToStepCallback = (0, _react.useCallback)(next => { _reactNative.Animated.timing(fadeAnim, { toValue: 0, duration: 180, useNativeDriver: _reactNative.Platform.OS !== 'web' }).start(() => { setCurrentStep(next); slideAnim.setValue(-40); _reactNative.Animated.parallel([_reactNative.Animated.timing(fadeAnim, { toValue: 1, duration: 220, useNativeDriver: _reactNative.Platform.OS !== 'web' }), _reactNative.Animated.spring(slideAnim, { toValue: 0, useNativeDriver: _reactNative.Platform.OS !== 'web', friction: 9 })]).start(); }); }, [fadeAnim, slideAnim]); const nextStep = (0, _react.useCallback)(() => { if (currentStep < totalSteps - 1) animateToStepCallback(currentStep + 1); }, [currentStep, totalSteps, animateToStepCallback]); const prevStep = (0, _react.useCallback)(() => { if (currentStep > 0) animateToStepCallback(currentStep - 1); }, [currentStep, animateToStepCallback]); const skipToAvatar = (0, _react.useCallback)(() => { if (avatarStepIndex >= 0) animateToStepCallback(avatarStepIndex); }, [avatarStepIndex, animateToStepCallback]); const finish = (0, _react.useCallback)(() => { if (onAuthenticated && currentUser) onAuthenticated(currentUser); }, [onAuthenticated, currentUser]); const openAvatarPicker = (0, _react.useCallback)(() => { // Ensure we're on the avatar step before opening picker if (avatarStepIndex >= 0 && currentStep !== avatarStepIndex) { animateToStepCallback(avatarStepIndex); } navigate('FileManagement', { selectMode: true, multiSelect: false, disabledMimeTypes: ['video/', 'audio/', 'application/pdf'], afterSelect: 'none', // Don't navigate away - stay on current screen onSelect: async file => { if (!file.contentType.startsWith('image/')) { _sonner.toast.error(t('editProfile.toasts.selectImage') || 'Please select an image file'); return; } try { // Update file visibility to public for avatar (skip if temporary asset ID) if (file.id && !file.id.startsWith('temp-')) { try { await oxyServices.assetUpdateVisibility(file.id, 'public'); console.log('[WelcomeNewUser] Avatar visibility updated to public'); } catch (visError) { // Only log non-404 errors (404 means asset doesn't exist yet, which is OK) if (visError?.response?.status !== 404) { console.warn('[WelcomeNewUser] Failed to update avatar visibility, continuing anyway:', visError); } } } // Update the avatar immediately in local state setSelectedAvatarId(file.id); // Update user in store await updateUser({ avatar: file.id }, oxyServices); _sonner.toast.success(t('editProfile.toasts.avatarUpdated') || 'Avatar updated'); // Ensure we stay on the avatar step if (avatarStepIndex >= 0 && currentStep !== avatarStepIndex) { animateToStepCallback(avatarStepIndex); } } catch (e) { _sonner.toast.error(e.message || t('editProfile.toasts.updateAvatarFailed') || 'Failed to update avatar'); } } }); }, [navigate, updateUser, oxyServices, currentStep, avatarStepIndex, animateToStepCallback, t]); const step = steps[currentStep]; const pillButtons = (0, _react.useMemo)(() => { if (currentStep === totalSteps - 1) { return [{ text: t('welcomeNew.actions.back') || 'Back', onPress: prevStep, icon: 'arrow-back', variant: 'transparent' }, { text: t('welcomeNew.actions.enter') || 'Enter', onPress: finish, icon: 'log-in-outline', variant: 'primary' }]; } if (currentStep === 0) { const arr = []; if (avatarStepIndex > 0) arr.push({ text: t('welcomeNew.actions.skip') || 'Skip', onPress: skipToAvatar, icon: 'play-skip-forward', variant: 'transparent' }); arr.push({ text: t('welcomeNew.actions.next') || 'Next', onPress: nextStep, icon: 'arrow-forward', variant: 'primary' }); return arr; } if (step.showAvatar) { return [{ text: t('welcomeNew.actions.back') || 'Back', onPress: prevStep, icon: 'arrow-back', variant: 'transparent' }, { text: avatarUri ? t('welcomeNew.actions.continue') || 'Continue' : t('welcomeNew.actions.skip') || 'Skip', onPress: nextStep, icon: 'arrow-forward', variant: 'primary' }]; } return [{ text: t('welcomeNew.actions.back') || 'Back', onPress: prevStep, icon: 'arrow-back', variant: 'transparent' }, { text: t('welcomeNew.actions.next') || 'Next', onPress: nextStep, icon: 'arrow-forward', variant: 'primary' }]; }, [currentStep, totalSteps, prevStep, nextStep, finish, skipToAvatar, avatarStepIndex, step.showAvatar, avatarUri]); return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.container, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.progressContainer, children: steps.map((s, i) => /*#__PURE__*/(0, _jsxRuntime.jsx)(AnimatedProgressDot, { isActive: i === currentStep, colors: colors, styles: styles }, s.key)) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, { style: { opacity: fadeAnim, transform: [{ translateX: slideAnim }] }, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, { contentContainerStyle: styles.scrollInner, showsVerticalScrollIndicator: false, children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.contentContainer, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.header, styles.sectionSpacing], children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.title, { color: colors.text }], children: step.title }), step.body && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.body, { color: colors.secondaryText }], children: step.body })] }), Array.isArray(step.bullets) && step.bullets.length > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.bulletContainer, styles.sectionSpacing], children: step.bullets.map(b => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.bulletRow, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "ellipse", size: 8, color: colors.primary, style: { marginTop: 6 } }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.bulletText, { color: colors.secondaryText }], children: b })] }, b)) }), step.showAvatar && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.avatarSection, styles.sectionSpacing], children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_Avatar.default, { size: 120, name: currentUser?.name?.full || currentUser?.name?.first || currentUser?.username, uri: avatarUri, theme: theme, backgroundColor: colors.primary + '20', style: styles.avatar }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, { style: [styles.changeAvatarButton, { backgroundColor: colors.primary }], onPress: openAvatarPicker, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "image-outline", size: 18, color: "#FFFFFF" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: styles.changeAvatarText, children: avatarUri ? t('welcomeNew.avatar.change') || 'Change Avatar' : t('welcomeNew.avatar.add') || 'Add Avatar' })] })] }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.sectionSpacing, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_GroupedPillButtons.default, { buttons: pillButtons, colors: colors }) })] }) }) })] }); }; const createStyles = theme => { const isDark = theme === 'dark'; const border = isDark ? '#333333' : '#E0E0E0'; return _reactNative.StyleSheet.create({ container: { width: '100%', paddingHorizontal: 20 }, scrollInner: { paddingBottom: 12, paddingTop: 0 }, contentContainer: { width: '100%', maxWidth: 420, alignSelf: 'center' }, sectionSpacing: { marginBottom: GAP }, header: { alignItems: 'flex-start', width: '100%', gap: INNER_GAP / 2 }, title: { fontSize: 42, fontFamily: _reactNative.Platform.OS === 'web' ? 'Phudu' : 'Phudu-Bold', fontWeight: _reactNative.Platform.OS === 'web' ? 'bold' : undefined, letterSpacing: -1, textAlign: 'left' }, body: { fontSize: 16, lineHeight: 22, textAlign: 'left', maxWidth: 320, alignSelf: 'flex-start' }, bulletContainer: { gap: INNER_GAP, width: '100%' }, bulletRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 10 }, bulletText: { flex: 1, fontSize: 15, lineHeight: 20 }, avatarSection: { width: '100%', alignItems: 'center' }, avatar: { marginBottom: INNER_GAP }, changeAvatarButton: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 18, paddingVertical: 10, borderRadius: 28, gap: 8, shadowOpacity: 0, shadowRadius: 0, shadowOffset: { width: 0, height: 0 }, elevation: 0, ...(_reactNative.Platform.OS === 'web' ? { boxShadow: 'none' } : null) }, changeAvatarText: { color: '#FFFFFF', fontSize: 15, fontWeight: '600' }, progressContainer: { flexDirection: 'row', width: '100%', justifyContent: 'center', marginTop: 24, // Space for bottom sheet handle (~20px) + small buffer marginBottom: 24 // Equal spacing below dots }, progressDot: { height: 6, width: 6, borderRadius: 3, marginHorizontal: 3, backgroundColor: border } }); }; var _default = exports.default = WelcomeNewUserScreen; //# sourceMappingURL=WelcomeNewUserScreen.js.map