live-react-native-elixir-test
Version:
React Native adapter for Phoenix LiveView reactivity
391 lines (390 loc) • 13.4 kB
JavaScript
"use strict";
/**
* RNCommandHandlers - Automatic handler for React Native commands sent from the server
*
* When the server sends RN.haptic(), RN.navigate(), etc., this class automatically
* executes the corresponding native React Native functionality.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.RNCommandHandlers = void 0;
class RNCommandHandlers {
constructor() {
this.dependencies = {};
this.checkDependencies();
}
/**
* Main handler for RN commands from the server
*/
async handleEvent(eventType, payload) {
try {
if (!eventType.startsWith('rn:')) {
return; // Not an RN command
}
const command = eventType.replace('rn:', '');
switch (command) {
case 'haptic':
await this.handleHaptic(payload);
break;
case 'navigate':
await this.handleNavigate(payload);
break;
case 'go_back':
await this.handleGoBack(payload);
break;
case 'reset_stack':
await this.handleResetStack(payload);
break;
case 'replace':
await this.handleReplace(payload);
break;
case 'vibrate':
await this.handleVibrate(payload);
break;
case 'notification':
await this.handleNotification(payload);
break;
case 'badge':
await this.handleBadge(payload);
break;
case 'show_toast':
await this.handleToast(payload);
break;
case 'show_alert':
await this.handleAlert(payload);
break;
case 'dismiss_keyboard':
await this.handleDismissKeyboard(payload);
break;
case 'show_loading':
await this.handleShowLoading(payload);
break;
case 'hide_loading':
await this.handleHideLoading(payload);
break;
default:
console.warn(`Unknown RN command: ${command}`);
}
}
catch (error) {
console.error('RN Command Handler Error:', error);
// Don't throw - continue execution
}
}
/**
* Handle haptic feedback
*/
async handleHaptic(payload) {
if (!this.dependencies['expo-haptics']) {
console.warn('expo-haptics not available');
return;
}
try {
const Haptics = require('expo-haptics');
const { type } = payload || {};
// Verify the module structure is correct
if (!Haptics || !Haptics.impactAsync || !Haptics.ImpactFeedbackStyle) {
console.warn('expo-haptics module incomplete');
return;
}
switch (type) {
case 'light':
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
break;
case 'medium':
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
break;
case 'heavy':
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
break;
case 'success':
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
break;
case 'warning':
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
break;
case 'error':
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
break;
case 'selection':
await Haptics.selectionAsync();
break;
default:
// Default to light impact
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
}
catch (error) {
console.warn('Haptic feedback failed:', error);
}
}
/**
* Handle navigation
*/
async handleNavigate(payload) {
if (!this.dependencies['@react-navigation/native']) {
console.warn('React Navigation not available');
return;
}
try {
const { useNavigation } = require('@react-navigation/native');
const navigation = useNavigation();
const { screen, params } = payload || {};
if (screen) {
navigation.navigate(screen, params);
}
}
catch (error) {
console.warn('Navigation failed:', error);
}
}
/**
* Handle go back
*/
async handleGoBack(payload) {
if (!this.dependencies['@react-navigation/native']) {
console.warn('React Navigation not available');
return;
}
try {
const { useNavigation } = require('@react-navigation/native');
const navigation = useNavigation();
navigation.goBack();
}
catch (error) {
console.warn('Go back failed:', error);
}
}
/**
* Handle reset stack
*/
async handleResetStack(payload) {
if (!this.dependencies['@react-navigation/native']) {
console.warn('React Navigation not available');
return;
}
try {
const { useNavigation } = require('@react-navigation/native');
const navigation = useNavigation();
const { routes } = payload || {};
if (routes && routes.length > 0) {
navigation.reset({
index: 0,
routes: routes,
});
}
}
catch (error) {
console.warn('Reset stack failed:', error);
}
}
/**
* Handle replace
*/
async handleReplace(payload) {
if (!this.dependencies['@react-navigation/native']) {
console.warn('React Navigation not available');
return;
}
try {
const { useNavigation } = require('@react-navigation/native');
const navigation = useNavigation();
const { screen, params } = payload || {};
if (screen) {
navigation.replace(screen, params);
}
}
catch (error) {
console.warn('Replace failed:', error);
}
}
/**
* Handle vibration
*/
async handleVibrate(payload) {
if (!this.dependencies['react-native']) {
console.warn('React Native Vibration not available');
return;
}
try {
const { Vibration } = require('react-native');
const { duration = 400, pattern } = payload || {};
if (pattern && Array.isArray(pattern)) {
Vibration.vibrate(pattern);
}
else {
Vibration.vibrate(duration);
}
}
catch (error) {
console.warn('Vibration failed:', error);
}
}
/**
* Handle notifications
*/
async handleNotification(payload) {
if (!this.dependencies['expo-notifications']) {
console.warn('expo-notifications not available');
return;
}
try {
const Notifications = require('expo-notifications');
const { title, body, data, trigger } = payload || {};
await Notifications.scheduleNotificationAsync({
content: {
title: title || '',
body: body || '',
data: data,
},
trigger: trigger || null, // null = immediate
});
}
catch (error) {
console.warn('Notification failed:', error);
}
}
/**
* Handle badge updates
*/
async handleBadge(payload) {
// TODO: Implement with expo-notifications setBadgeCountAsync
// For now, just log the badge update
try {
const { count } = payload || {};
console.log(`Badge count update: ${count}`);
// Future implementation:
// const Notifications = require('expo-notifications');
// await Notifications.setBadgeCountAsync(count || 0);
}
catch (error) {
console.warn('Badge update failed:', error);
}
}
/**
* Handle toast messages
*/
async handleToast(payload) {
if (!this.dependencies['react-native']) {
console.warn('React Native not available for toast');
return;
}
try {
const { ToastAndroid, Alert, Platform } = require('react-native');
const { message, duration = 'short' } = payload || {};
if (Platform.OS === 'android') {
const toastDuration = duration === 'long' ? ToastAndroid.LONG : ToastAndroid.SHORT;
ToastAndroid.show(message || '', toastDuration);
}
else {
// iOS fallback to alert
Alert.alert('', message || '');
}
}
catch (error) {
console.warn('Toast failed:', error);
}
}
/**
* Handle alert dialogs
*/
async handleAlert(payload) {
if (!this.dependencies['react-native']) {
console.warn('React Native Alert not available');
return;
}
try {
const { Alert } = require('react-native');
const { title, message, buttons } = payload || {};
Alert.alert(title || '', message || '', buttons);
}
catch (error) {
console.warn('Alert failed:', error);
}
}
/**
* Handle keyboard dismissal
*/
async handleDismissKeyboard(payload) {
if (!this.dependencies['react-native']) {
console.warn('React Native Keyboard not available');
return;
}
try {
const { Keyboard } = require('react-native');
Keyboard.dismiss();
}
catch (error) {
console.warn('Keyboard dismiss failed:', error);
}
}
/**
* Handle show loading
*/
async handleShowLoading(payload) {
try {
const { message } = payload || {};
console.log(`Show loading: ${message || 'Loading...'}`);
// TODO: Integrate with a loading library like react-native-loading-spinner-overlay
// or a custom loading context
}
catch (error) {
console.warn('Show loading failed:', error);
}
}
/**
* Handle hide loading
*/
async handleHideLoading(payload) {
try {
console.log('Hide loading');
// TODO: Integrate with a loading library
}
catch (error) {
console.warn('Hide loading failed:', error);
}
}
/**
* Check which React Native dependencies are available
* Metro-compatible version using static requires
*/
checkDependencies() {
// Test react-native
try {
const RN = require('react-native');
this.dependencies['react-native'] = !!(RN && RN.Alert && RN.Keyboard && RN.Vibration);
}
catch (error) {
console.warn('react-native not available:', error instanceof Error ? error.message : String(error));
this.dependencies['react-native'] = false;
}
// Test expo-haptics
try {
const Haptics = require('expo-haptics');
this.dependencies['expo-haptics'] = !!(Haptics && Haptics.impactAsync && Haptics.ImpactFeedbackStyle);
}
catch (error) {
console.warn('expo-haptics not available:', error instanceof Error ? error.message : String(error));
this.dependencies['expo-haptics'] = false;
}
// Test @react-navigation/native
try {
const Nav = require('@react-navigation/native');
this.dependencies['@react-navigation/native'] = !!(Nav && Nav.useNavigation);
}
catch (error) {
console.warn('@react-navigation/native not available:', error instanceof Error ? error.message : String(error));
this.dependencies['@react-navigation/native'] = false;
}
// Test expo-notifications
try {
const Notifications = require('expo-notifications');
this.dependencies['expo-notifications'] = !!(Notifications && Notifications.scheduleNotificationAsync);
}
catch (error) {
console.warn('expo-notifications not available:', error instanceof Error ? error.message : String(error));
this.dependencies['expo-notifications'] = false;
}
return { ...this.dependencies };
}
}
exports.RNCommandHandlers = RNCommandHandlers;