now deep linking completed and loader also added for the app
362
App.tsx
@ -1,131 +1,269 @@
|
||||
/**
|
||||
* Sample React Native App
|
||||
* https://github.com/facebook/react-native
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
import React, { JSX, useEffect, useState } from 'react';
|
||||
import { SafeAreaView, StyleSheet, Platform, StatusBar, AppState, View, Text, ActivityIndicator, Animated } from 'react-native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
|
||||
import React from 'react';
|
||||
import type {PropsWithChildren} from 'react';
|
||||
// Utility imports
|
||||
import {
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
createNotificationChannel,
|
||||
configureNotificationSettings,
|
||||
testNotification
|
||||
} from './utilities/notificationUtils';
|
||||
import {
|
||||
Colors,
|
||||
DebugInstructions,
|
||||
Header,
|
||||
LearnMoreLinks,
|
||||
ReloadInstructions,
|
||||
} from 'react-native/Libraries/NewAppScreen';
|
||||
createDeepLinkHandler,
|
||||
NotificationData
|
||||
} from './utilities/deepLinkUtils';
|
||||
import {
|
||||
createWebViewHandler
|
||||
} from './utilities/webViewUtils';
|
||||
import {
|
||||
createCookieHandler
|
||||
} from './utilities/cookieUtils';
|
||||
import {
|
||||
createFCMHandler,
|
||||
NotificationCallbacks
|
||||
} from './utilities/fcmUtils';
|
||||
import { ALLOWED_DOMAIN } from './utilities/constants';
|
||||
import { CustomLoader, createOdooLoader, createNavigationLoader, createChatLoader } from './utilities/loaderUtils';
|
||||
|
||||
type SectionProps = PropsWithChildren<{
|
||||
title: string;
|
||||
}>;
|
||||
|
||||
function Section({children, title}: SectionProps): React.JSX.Element {
|
||||
const isDarkMode = useColorScheme() === 'dark';
|
||||
return (
|
||||
<View style={styles.sectionContainer}>
|
||||
<Text
|
||||
style={[
|
||||
styles.sectionTitle,
|
||||
{
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
},
|
||||
]}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.sectionDescription,
|
||||
{
|
||||
color: isDarkMode ? Colors.light : Colors.dark,
|
||||
},
|
||||
]}>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
// Suppress Firebase deprecation warnings (temporary until Firebase v22+ is stable)
|
||||
const originalWarn = console.warn;
|
||||
console.warn = (...args) => {
|
||||
if (args[0] && typeof args[0] === 'string' &&
|
||||
args[0].includes('This method is deprecated') &&
|
||||
args[0].includes('Firebase')) {
|
||||
return; // Suppress only Firebase deprecation warnings
|
||||
}
|
||||
originalWarn.apply(console, args);
|
||||
};
|
||||
// @ts-ignore
|
||||
import PushNotification from 'react-native-push-notification';
|
||||
|
||||
function App(): React.JSX.Element {
|
||||
const isDarkMode = useColorScheme() === 'dark';
|
||||
/**
|
||||
* Main App Component
|
||||
* Handles FCM notifications, WebView management, and deep linking
|
||||
*/
|
||||
export default function App(): JSX.Element {
|
||||
|
||||
const backgroundStyle = {
|
||||
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
|
||||
// Track app state to avoid duplicate notifications
|
||||
const [appState, setAppState] = React.useState(AppState.currentState);
|
||||
|
||||
// WebView reference for accessing cookies
|
||||
const webViewRef = React.useRef<WebView>(null);
|
||||
|
||||
// Deep linking state
|
||||
const [currentChannel, setCurrentChannel] = React.useState<string | null>(null);
|
||||
const webViewUrlRef = React.useRef(`${ALLOWED_DOMAIN}/web`);
|
||||
const [webViewKey, setWebViewKey] = React.useState(0); // Force re-render when URL changes
|
||||
const [isNavigating, setIsNavigating] = React.useState(false);
|
||||
const [navigationAttempts, setNavigationAttempts] = React.useState(0);
|
||||
|
||||
// State to store cookies
|
||||
const [webViewCookies, setWebViewCookies] = React.useState<string | null>(null);
|
||||
|
||||
// Loading state
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [loadingText, setLoadingText] = useState('Connecting to T4B...');
|
||||
const [loaderType, setLoaderType] = useState<'odoo' | 'navigation' | 'chat'>('odoo');
|
||||
|
||||
// Helper function to update WebView URL immediately (without loader)
|
||||
const updateWebViewUrl = (newUrl: string) => {
|
||||
console.log('🔗 Updating WebView URL immediately:', newUrl);
|
||||
// Don't show loader for navigation to avoid flickering
|
||||
webViewUrlRef.current = newUrl;
|
||||
setWebViewKey(prev => prev + 1); // Force WebView re-render
|
||||
console.log('✅ WebView URL updated:', newUrl);
|
||||
};
|
||||
|
||||
/*
|
||||
* To keep the template simple and small we're adding padding to prevent view
|
||||
* from rendering under the System UI.
|
||||
* For bigger apps the recommendation is to use `react-native-safe-area-context`:
|
||||
* https://github.com/AppAndFlow/react-native-safe-area-context
|
||||
*
|
||||
* You can read more about it here:
|
||||
* https://github.com/react-native-community/discussions-and-proposals/discussions/827
|
||||
*/
|
||||
const safePadding = '5%';
|
||||
// Create utility handlers
|
||||
const cookieHandler = createCookieHandler(setWebViewCookies);
|
||||
const webViewHandler = createWebViewHandler(webViewRef, updateWebViewUrl, {
|
||||
currentChannel,
|
||||
isNavigating
|
||||
});
|
||||
|
||||
// Navigation state for deep link handler
|
||||
const navigationState = {
|
||||
currentChannel,
|
||||
isNavigating,
|
||||
navigationAttempts
|
||||
};
|
||||
|
||||
const deepLinkHandler = createDeepLinkHandler(
|
||||
updateWebViewUrl,
|
||||
setIsNavigating,
|
||||
setCurrentChannel,
|
||||
setNavigationAttempts,
|
||||
navigationState
|
||||
);
|
||||
|
||||
// FCM notification callbacks
|
||||
const fcmCallbacks: NotificationCallbacks = {
|
||||
onForegroundNotification: (data: NotificationData) => {
|
||||
deepLinkHandler.handleForegroundBackgroundDeepLink(data);
|
||||
},
|
||||
onBackgroundNotification: (data: NotificationData) => {
|
||||
deepLinkHandler.handleForegroundBackgroundDeepLink(data);
|
||||
},
|
||||
onKilledAppNotification: (data: NotificationData) => {
|
||||
deepLinkHandler.handleKilledAppDeepLink(data);
|
||||
}
|
||||
};
|
||||
|
||||
const fcmHandler = createFCMHandler(
|
||||
() => cookieHandler.getWebViewCookies(),
|
||||
fcmCallbacks
|
||||
);
|
||||
|
||||
// Handle app state changes
|
||||
useEffect(() => {
|
||||
const handleAppStateChange = (nextAppState: any) => {
|
||||
console.log('App state changed:', appState, '->', nextAppState);
|
||||
setAppState(nextAppState);
|
||||
};
|
||||
|
||||
const subscription = AppState.addEventListener('change', handleAppStateChange);
|
||||
return () => subscription?.remove();
|
||||
}, [appState]);
|
||||
|
||||
// Initialize FCM and WebView
|
||||
useEffect(() => {
|
||||
// Configure push notifications
|
||||
createNotificationChannel();
|
||||
configureNotificationSettings();
|
||||
|
||||
// Initialize FCM notifications
|
||||
fcmHandler.initializeNotifications();
|
||||
|
||||
// Start periodic cookie refresh
|
||||
const cleanupCookieRefresh = cookieHandler.refreshWebViewCookies();
|
||||
|
||||
// Setup FCM message listeners
|
||||
const cleanupFCMListeners = fcmHandler.setupMessageListeners(appState);
|
||||
|
||||
// Setup test functions for debugging
|
||||
setupTestFunctions();
|
||||
|
||||
return () => {
|
||||
cleanupCookieRefresh();
|
||||
cleanupFCMListeners();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Setup test functions for debugging
|
||||
const setupTestFunctions = () => {
|
||||
if (typeof global !== 'undefined') {
|
||||
(global as any).testNotification = testNotification;
|
||||
(global as any).refreshCookies = () => {
|
||||
console.log('🔄 Manual cookie refresh triggered');
|
||||
cookieHandler.getWebViewCookies();
|
||||
};
|
||||
(global as any).checkCookies = () => {
|
||||
console.log('🔍 Current cookie state:', webViewCookies);
|
||||
console.log('🔍 WebView ref available:', !!webViewRef.current);
|
||||
cookieHandler.getWebViewCookies();
|
||||
};
|
||||
(global as any).testDeepLink = (channelId: string) => {
|
||||
console.log('🔗 Testing deep link for channel:', channelId);
|
||||
deepLinkHandler.navigateToChannel(channelId);
|
||||
};
|
||||
(global as any).testDeepLinkUrl = (deepLink: string) => {
|
||||
console.log('🔗 Testing deep link URL:', deepLink);
|
||||
deepLinkHandler.handleDeepLink(deepLink);
|
||||
};
|
||||
(global as any).simulateNotification = (channelId: string) => {
|
||||
console.log('🔗 Simulating notification tap for channel:', channelId);
|
||||
setTimeout(() => {
|
||||
deepLinkHandler.handleForegroundBackgroundDeepLink({ channel_id: channelId });
|
||||
}, 100);
|
||||
};
|
||||
(global as any).testWebViewUrlUpdate = (channelId: string) => {
|
||||
console.log('🔗 Testing WebView URL update for channel:', channelId);
|
||||
deepLinkHandler.handleForegroundBackgroundDeepLink({ channel_id: channelId });
|
||||
};
|
||||
(global as any).debugWebView = () => {
|
||||
webViewHandler.injectDebugScript();
|
||||
};
|
||||
(global as any).navigateToChannel = (channelId: string) => {
|
||||
console.log('🔗 Direct navigation to channel:', channelId);
|
||||
deepLinkHandler.navigateToChannel(channelId);
|
||||
};
|
||||
(global as any).updateWebViewUrl = (url: string) => {
|
||||
console.log('🔗 Direct WebView URL update:', url);
|
||||
updateWebViewUrl(url);
|
||||
};
|
||||
|
||||
console.log('🧪 Test functions available:');
|
||||
console.log(' - global.testNotification()');
|
||||
console.log(' - global.refreshCookies()');
|
||||
console.log(' - global.checkCookies()');
|
||||
console.log(' - global.testDeepLink(channelId) - For killed app deep linking');
|
||||
console.log(' - global.testWebViewUrlUpdate(channelId) - For foreground/background URL update');
|
||||
console.log(' - global.testDeepLinkUrl("myapp://chat/123")');
|
||||
console.log(' - global.simulateNotification(channelId)');
|
||||
console.log(' - global.debugWebView() - Debug WebView state and elements');
|
||||
console.log(' - global.navigateToChannel(channelId) - Direct navigation using WebView URL update');
|
||||
console.log(' - global.updateWebViewUrl(url) - Immediate WebView URL update');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle WebView navigation
|
||||
const handleNavigation = (request: any): boolean => {
|
||||
return webViewHandler.handleNavigation(request);
|
||||
};
|
||||
|
||||
|
||||
// Handle WebView messages
|
||||
const handleWebViewMessage = (event: any) => {
|
||||
webViewHandler.handleWebViewMessage(event, setWebViewCookies);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={backgroundStyle}>
|
||||
<StatusBar
|
||||
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
|
||||
backgroundColor={backgroundStyle.backgroundColor}
|
||||
<SafeAreaView style={styles.container}>
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
source={{ uri: webViewUrlRef.current }}
|
||||
style={styles.webview}
|
||||
key={webViewKey}
|
||||
javaScriptEnabled
|
||||
domStorageEnabled
|
||||
startInLoadingState={false} // Use custom loader instead
|
||||
originWhitelist={['*']}
|
||||
onShouldStartLoadWithRequest={handleNavigation}
|
||||
onMessage={handleWebViewMessage}
|
||||
onLoadStart={() => {
|
||||
console.log('🔄 WebView started loading');
|
||||
// Don't show loader during WebView navigation to avoid flickering
|
||||
}}
|
||||
onLoadEnd={() => {
|
||||
console.log('✅ WebView finished loading');
|
||||
setIsLoading(false); // Only hide initial loader
|
||||
webViewHandler.handleWebViewLoadEnd(currentChannel, isNavigating);
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.error('❌ WebView error:', error);
|
||||
setIsLoading(false);
|
||||
setLoadingText('Connection error');
|
||||
}}
|
||||
//@ts-ignore
|
||||
onConsoleMessage={(event: any) => {
|
||||
console.log('🌐 WebView Console:', event.nativeEvent.message);
|
||||
}}
|
||||
/>
|
||||
<ScrollView
|
||||
style={backgroundStyle}>
|
||||
<View style={{paddingRight: safePadding}}>
|
||||
<Header/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: isDarkMode ? Colors.black : Colors.white,
|
||||
paddingHorizontal: safePadding,
|
||||
paddingBottom: safePadding,
|
||||
}}>
|
||||
<Section title="Step One">
|
||||
Edit <Text style={styles.highlight}>App.tsx</Text> to change this
|
||||
screen and then come back to see your edits.
|
||||
</Section>
|
||||
<Section title="See Your Changes">
|
||||
<ReloadInstructions />
|
||||
</Section>
|
||||
<Section title="Debug">
|
||||
<DebugInstructions />
|
||||
</Section>
|
||||
<Section title="Learn More">
|
||||
Read the docs to discover what to do next:
|
||||
</Section>
|
||||
<LearnMoreLinks />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Dynamic Beautiful Custom Loader */}
|
||||
{loaderType === 'odoo' && createOdooLoader(isLoading, loadingText)}
|
||||
{loaderType === 'navigation' && createNavigationLoader(isLoading, loadingText)}
|
||||
{loaderType === 'chat' && createChatLoader(isLoading, loadingText)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
sectionContainer: {
|
||||
marginTop: 32,
|
||||
paddingHorizontal: 24,
|
||||
container: {
|
||||
flex: 1,
|
||||
// paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight ?? 0 : 0,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: '600',
|
||||
},
|
||||
sectionDescription: {
|
||||
marginTop: 8,
|
||||
fontSize: 18,
|
||||
fontWeight: '400',
|
||||
},
|
||||
highlight: {
|
||||
fontWeight: '700',
|
||||
webview: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default App;
|
||||
|
||||
384
FCM_NOTIFICATION_IMPLEMENTATION_GUIDE.md
Normal file
@ -0,0 +1,384 @@
|
||||
# FCM Notification Module Implementation Guide
|
||||
|
||||
This document contains all the changes made to implement Firebase Cloud Messaging (FCM) notifications in the T4B_Chat React Native app. Use this guide to replicate the implementation in your production project.
|
||||
|
||||
## 📦 Package Dependencies Added
|
||||
|
||||
### package.json Dependencies
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@react-native-firebase/app": "^23.4.0",
|
||||
"@react-native-firebase/messaging": "^23.4.0",
|
||||
"@react-native-cookies/cookies": "^6.2.0",
|
||||
"react-native-device-info": "^10.11.0",
|
||||
"react-native-permissions": "^5.2.4",
|
||||
"react-native-push-notification": "^8.1.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Installation Commands
|
||||
```bash
|
||||
npm install @react-native-firebase/app @react-native-firebase/messaging
|
||||
npm install @react-native-cookies/cookies
|
||||
npm install react-native-device-info
|
||||
npm install react-native-permissions
|
||||
npm install react-native-push-notification
|
||||
```
|
||||
|
||||
## 🔧 Android Configuration
|
||||
|
||||
### 1. Root build.gradle (android/build.gradle)
|
||||
```gradle
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "35.0.0"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 35
|
||||
targetSdkVersion = 35
|
||||
ndkVersion = "27.1.12297006"
|
||||
kotlinVersion = "2.0.21"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
|
||||
classpath("com.google.gms:google-services:4.3.15") // ← ADD THIS
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. App build.gradle (android/app/build.gradle)
|
||||
```gradle
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
apply plugin: 'com.google.gms.google-services' // ← ADD THIS
|
||||
|
||||
android {
|
||||
// ... existing config ...
|
||||
|
||||
// Fix for react-native-push-notification duplicate classes
|
||||
configurations.all {
|
||||
exclude group: 'com.android.support', module: 'support-compat'
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.t4b_chat"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
// AndroidX dependencies for react-native-push-notification compatibility
|
||||
implementation 'androidx.core:core:1.16.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
|
||||
// ... rest of dependencies ...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. AndroidManifest.xml (android/app/src/main/AndroidManifest.xml)
|
||||
```xml
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- ← ADD THIS -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" /> <!-- ← ADD THIS -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" /> <!-- ← ADD THIS -->
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:supportsRtl="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- FCM Service --> <!-- ← ADD THIS ENTIRE SERVICE BLOCK -->
|
||||
<service
|
||||
android:name="com.google.firebase.messaging.FirebaseMessagingService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
### 4. Google Services Configuration
|
||||
- Add `google-services.json` file to `android/app/` directory
|
||||
- This file comes from your Firebase project console
|
||||
|
||||
## 📱 iOS Configuration
|
||||
|
||||
### 1. Podfile (ios/Podfile)
|
||||
```ruby
|
||||
# Resolve react_native_pods.rb with node to allow for hoisting
|
||||
require Pod::Executable.execute_command('node', ['-p',
|
||||
'require.resolve(
|
||||
"react-native/scripts/react_native_pods.rb",
|
||||
{paths: [process.argv[1]]},
|
||||
)', __dir__]).strip
|
||||
|
||||
platform :ios, min_ios_version_supported
|
||||
prepare_react_native_project!
|
||||
|
||||
linkage = ENV['USE_FRAMEWORKS']
|
||||
if linkage != nil
|
||||
Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
|
||||
use_frameworks! :linkage => linkage.to_sym
|
||||
end
|
||||
|
||||
target 'T4B_Chat' do
|
||||
config = use_native_modules!
|
||||
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||
)
|
||||
|
||||
post_install do |installer|
|
||||
react_native_post_install(
|
||||
installer,
|
||||
config[:reactNativePath],
|
||||
:mac_catalyst_enabled => false,
|
||||
)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### 2. iOS Google Services Configuration
|
||||
- Add `GoogleService-Info.plist` to your iOS project
|
||||
- This file comes from your Firebase project console
|
||||
|
||||
## 📁 New Utility Files Created
|
||||
|
||||
### 1. utilities/permissionUtils.ts
|
||||
Complete permission management utility for handling notification permissions across platforms.
|
||||
|
||||
Key functions:
|
||||
- `needsNotificationPermission()` - Check if device needs explicit permission
|
||||
- `requestNotificationPermission()` - Request notification permission
|
||||
- `checkNotificationPermission()` - Check current permission status
|
||||
- `showPermissionExplanation()` - Show user-friendly explanation dialog
|
||||
- `showSettingsDialog()` - Direct user to app settings
|
||||
|
||||
### 2. utilities/notificationUtils.ts
|
||||
Complete notification management utility for handling local notifications.
|
||||
|
||||
Key functions:
|
||||
- `createNotificationChannel()` - Create Android notification channel
|
||||
- `configureNotificationSettings()` - Configure push notification settings
|
||||
- `showLocalNotification()` - Display local notifications
|
||||
- `shouldShowCustomNotification()` - Determine when to show notifications
|
||||
- `testNotification()` - Test notification function
|
||||
- `cancelAllNotifications()` - Cancel all pending notifications
|
||||
|
||||
## 🔄 App.tsx Changes
|
||||
|
||||
### Key Imports Added
|
||||
```typescript
|
||||
import messaging, { FirebaseMessagingTypes, AuthorizationStatus } from '@react-native-firebase/messaging';
|
||||
import { getApp } from '@react-native-firebase/app';
|
||||
import {
|
||||
requestNotificationPermission,
|
||||
checkNotificationPermission,
|
||||
needsNotificationPermission,
|
||||
showPermissionExplanation,
|
||||
showSettingsDialog,
|
||||
PermissionResult
|
||||
} from './utilities/permissionUtils';
|
||||
import { RESULTS } from 'react-native-permissions';
|
||||
import {
|
||||
showLocalNotification,
|
||||
createNotificationChannel,
|
||||
configureNotificationSettings,
|
||||
shouldShowCustomNotification,
|
||||
testNotification
|
||||
} from './utilities/notificationUtils';
|
||||
import PushNotification from 'react-native-push-notification';
|
||||
```
|
||||
|
||||
### Key Features Implemented
|
||||
|
||||
1. **App State Tracking**
|
||||
- Tracks app state changes to avoid duplicate notifications
|
||||
- Uses `AppState.addEventListener` for state monitoring
|
||||
|
||||
2. **FCM Message Handling**
|
||||
- `onMessage` listener for foreground messages
|
||||
- `getInitialNotification` for app launch from notification
|
||||
- `onNotificationOpenedApp` for background notification taps
|
||||
|
||||
3. **Permission Management**
|
||||
- Comprehensive permission checking and requesting
|
||||
- Platform-specific permission handling (Android 13+ vs older versions)
|
||||
- User-friendly permission explanation dialogs
|
||||
|
||||
4. **Token Management**
|
||||
- FCM token retrieval and refresh handling
|
||||
- Token sending to backend (commented out - needs implementation)
|
||||
|
||||
5. **Notification Display**
|
||||
- Custom notification display when app is active
|
||||
- Fallback to system notifications when app is backgrounded
|
||||
- Test notification functionality for debugging
|
||||
|
||||
6. **Firebase Warning Suppression**
|
||||
- Temporary suppression of Firebase deprecation warnings
|
||||
|
||||
## 🚀 Setup Instructions for Production
|
||||
|
||||
### 1. Install Dependencies
|
||||
```bash
|
||||
npm install @react-native-firebase/app @react-native-firebase/messaging
|
||||
npm install react-native-permissions
|
||||
npm install react-native-push-notification
|
||||
```
|
||||
|
||||
### 2. Firebase Project Setup
|
||||
1. Create Firebase project at https://console.firebase.google.com
|
||||
2. Add Android app with package name matching your `applicationId`
|
||||
3. Add iOS app with bundle identifier
|
||||
4. Download configuration files:
|
||||
- `google-services.json` for Android
|
||||
- `GoogleService-Info.plist` for iOS
|
||||
|
||||
### 3. Android Setup
|
||||
1. Add Google Services plugin to build.gradle files
|
||||
2. Add permissions to AndroidManifest.xml
|
||||
3. Add FCM service to AndroidManifest.xml
|
||||
4. Add AndroidX dependencies for compatibility
|
||||
|
||||
### 4. iOS Setup
|
||||
1. Add GoogleService-Info.plist to Xcode project
|
||||
2. Run `cd ios && pod install`
|
||||
3. Enable Push Notifications capability in Xcode
|
||||
|
||||
### 5. Code Integration
|
||||
1. Copy both utility files to your project
|
||||
2. Integrate App.tsx changes
|
||||
3. Update package.json dependencies
|
||||
4. Test notification functionality
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Test Notification Function
|
||||
The implementation includes a test notification function accessible via:
|
||||
- URL navigation to `test-notification` endpoint
|
||||
- Global function `global.testNotification()` in development
|
||||
- Console logging for debugging
|
||||
|
||||
### Debug Features
|
||||
- Comprehensive console logging throughout the notification flow
|
||||
- Permission status logging
|
||||
- FCM token logging
|
||||
- App state change logging
|
||||
|
||||
## 🔗 Backend Integration
|
||||
|
||||
### FCM Token Registration
|
||||
The app automatically sends FCM tokens to your Odoo backend using the following endpoint:
|
||||
|
||||
**Endpoint**: `POST https://your-odoo.com/fcm/register`
|
||||
|
||||
**Headers**:
|
||||
```json
|
||||
{
|
||||
"Content-Type": "application/json",
|
||||
"Cookie": "session_id=YOUR_SESSION_COOKIE"
|
||||
}
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"token": "FCM_TOKEN_STRING",
|
||||
"device_name": "iPhone 12",
|
||||
"device_type": "ios",
|
||||
"app_version": "1.0.0",
|
||||
"os_version": "15.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Device Information Collection
|
||||
The implementation automatically collects device information using `react-native-device-info`:
|
||||
- `device_name`: Device model name (e.g., "iPhone 12", "Samsung Galaxy S21")
|
||||
- `device_type`: Platform ("ios" or "android")
|
||||
- `app_version`: App version from package.json
|
||||
- `os_version`: Operating system version
|
||||
|
||||
### Session Management
|
||||
**Native Cookie Extraction**: The implementation uses `@react-native-cookies/cookies` to automatically extract session cookies from the native cookie store. The `getWebViewCookies()` function:
|
||||
|
||||
1. **Accesses native cookie store** using `@react-native-cookies/cookies` library
|
||||
2. **Retrieves all cookies** for the Odoo domain
|
||||
3. **Filters and extracts session_id** from available cookies
|
||||
4. **Updates automatically** every 30 seconds to keep cookies fresh
|
||||
5. **Validates cookies** before sending FCM token registration
|
||||
|
||||
The system automatically handles session management without manual intervention and is much more reliable than JavaScript injection methods.
|
||||
|
||||
### Token Refresh Handling
|
||||
The app automatically handles FCM token refresh and sends updated tokens to the backend whenever they change.
|
||||
|
||||
## ⚠️ Important Notes
|
||||
|
||||
1. **Backend Integration**: The `sendTokenToBackend()` function is implemented and automatically sends FCM tokens to your Odoo backend at `/fcm/register` endpoint.
|
||||
|
||||
2. **Session Management**: The implementation automatically extracts session cookies from the WebView, so no manual session management is required.
|
||||
|
||||
3. **Device Information**: The implementation uses `react-native-device-info` to automatically collect device information (device name, type, app version, OS version).
|
||||
|
||||
4. **Firebase Warnings**: Temporary warning suppression is in place until Firebase v22+ is stable.
|
||||
|
||||
5. **Permission Handling**: The implementation handles Android 13+ permission requirements automatically.
|
||||
|
||||
6. **Production Considerations**:
|
||||
- Update signing configuration for release builds
|
||||
- Test on both Android and iOS devices
|
||||
- Verify notification delivery in all app states
|
||||
- Ensure WebView loads and user can log in to extract session cookies
|
||||
|
||||
## 📋 Checklist for Production Migration
|
||||
|
||||
- [ ] Install all required npm packages
|
||||
- [ ] Add Google Services configuration files
|
||||
- [ ] Update Android build.gradle files
|
||||
- [ ] Update AndroidManifest.xml with permissions and FCM service
|
||||
- [ ] Copy utility files (permissionUtils.ts, notificationUtils.ts)
|
||||
- [ ] Integrate App.tsx changes
|
||||
- [ ] Test on Android device (API 33+ for permission testing)
|
||||
- [ ] Test on iOS device
|
||||
- [ ] Implement backend token registration
|
||||
- [ ] Test notification delivery in all app states
|
||||
- [ ] Remove debug/test code before production release
|
||||
|
||||
This implementation provides a complete FCM notification system with proper permission handling, platform-specific optimizations, and comprehensive error handling.
|
||||
@ -1,6 +1,7 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
@ -58,6 +59,7 @@ react {
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = false
|
||||
def enableSeparateBuildPerCPUArchitecture = true
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
@ -78,6 +80,19 @@ android {
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace "com.t4b_chat"
|
||||
|
||||
// Fix for react-native-push-notification duplicate classes
|
||||
configurations.all {
|
||||
exclude group: 'com.android.support', module: 'support-compat'
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
include 'armeabi-v7a', 'arm64-v8a', 'x86'
|
||||
universalApk false
|
||||
}
|
||||
}
|
||||
defaultConfig {
|
||||
applicationId "com.t4b_chat"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
@ -111,9 +126,15 @@ dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
// AndroidX dependencies for react-native-push-notification compatibility
|
||||
implementation 'androidx.core:core:1.16.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")
|
||||
29
android/app/google-services.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "640541795037",
|
||||
"project_id": "t4b-chat",
|
||||
"storage_bucket": "t4b-chat.firebasestorage.app"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:640541795037:android:b40cb90983466cdafd3134",
|
||||
"android_client_info": {
|
||||
"package_name": "com.t4b_chat"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCQ1zgB0uVL3VgRnsPrbdYQOneOJzc2AAM"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@ -1,6 +1,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
@ -9,6 +12,8 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
networkSecurityConfig="@xml/network_security_config"
|
||||
android:supportsRtl="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@ -22,5 +27,14 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- FCM Service -->
|
||||
<service
|
||||
android:name="com.google.firebase.messaging.FirebaseMessagingService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
BIN
android/app/src/main/playstore.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
11
android/app/src/main/res/drawable/ic_notification.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorOnPrimary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 11 KiB |
@ -4,6 +4,7 @@
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
14
android/app/src/main/res/xml/network_security_config.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">192.168.1.12</domain>
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
@ -15,6 +15,7 @@ buildscript {
|
||||
classpath("com.android.tools.build:gradle")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
|
||||
classpath("com.google.gms:google-services:4.3.15")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,6 +21,8 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
@ -32,7 +34,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchEnabled=true
|
||||
newArchEnabled=false
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
2285
package-lock.json
generated
30
package.json
@ -10,8 +10,36 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
"@react-native-cookies/cookies": "^6.2.1",
|
||||
"@react-native-firebase/app": "^23.4.0",
|
||||
"@react-native-firebase/messaging": "^23.4.0",
|
||||
"@react-native-vector-icons/common": "^12.3.0",
|
||||
"@react-navigation/bottom-tabs": "^7.4.7",
|
||||
"@react-navigation/native": "^7.1.17",
|
||||
"@react-navigation/native-stack": "^7.3.26",
|
||||
"@react-navigation/stack": "^7.4.8",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"apisauce": "^3.2.0",
|
||||
"react": "19.0.0",
|
||||
"react-native": "0.79.0"
|
||||
"react-native": "0.79.0",
|
||||
"react-native-device-info": "^10.3.0",
|
||||
"react-native-element-dropdown": "^2.12.4",
|
||||
"react-native-gesture-handler": "^2.28.0",
|
||||
"react-native-linear-gradient": "^2.8.3",
|
||||
"react-native-permissions": "^5.2.4",
|
||||
"react-native-push-notification": "^8.1.1",
|
||||
"react-native-raw-bottom-sheet": "^3.0.0",
|
||||
"react-native-reanimated": "^3.19.1",
|
||||
"react-native-safe-area-context": "^5.6.1",
|
||||
"react-native-screens": "^4.16.0",
|
||||
"react-native-svg": "^15.12.1",
|
||||
"react-native-toast-message": "^2.2.1",
|
||||
"react-native-vector-icons": "^10.3.0",
|
||||
"react-native-webview": "^13.16.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"redux-persist": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
|
||||
134
utilities/README.md
Normal file
@ -0,0 +1,134 @@
|
||||
# Utilities
|
||||
|
||||
This directory contains organized utility functions and modules for the T4B Chat application.
|
||||
|
||||
## Files
|
||||
|
||||
### notificationUtils.ts
|
||||
Contains utility functions for creating notification channels, configuring settings, and showing local notifications.
|
||||
|
||||
### permissionUtils.ts
|
||||
Handles notification permission requests and checks for Android and iOS platforms.
|
||||
|
||||
### deepLinkUtils.ts
|
||||
Handles deep linking functionality for FCM notifications and chat navigation.
|
||||
- `DeepLinkHandler` class for managing deep links
|
||||
- Methods for handling killed app, foreground, and background deep links
|
||||
- Channel ID extraction and URL building utilities
|
||||
|
||||
### webViewUtils.ts
|
||||
Manages WebView functionality, navigation, and message handling.
|
||||
- `WebViewHandler` class for WebView management
|
||||
- Navigation request handling and validation
|
||||
- Message processing and debug script injection
|
||||
|
||||
### cookieUtils.ts
|
||||
Handles cookie management for WebView authentication.
|
||||
- `CookieHandler` class for cookie operations
|
||||
- Platform-specific cookie retrieval (iOS/Android)
|
||||
- Session cookie validation and refresh functionality
|
||||
|
||||
### fcmUtils.ts
|
||||
Manages Firebase Cloud Messaging (FCM) functionality.
|
||||
- `FCMHandler` class for FCM operations
|
||||
- Token management and backend communication
|
||||
- Message handling for different app states
|
||||
|
||||
### constants.ts
|
||||
Centralized configuration constants for the entire application.
|
||||
- Domain configuration
|
||||
- FCM endpoints
|
||||
- Navigation delays
|
||||
- URL patterns
|
||||
- Platform configuration
|
||||
- Notification settings
|
||||
|
||||
### loaderUtils.tsx
|
||||
Beautiful custom loading components with animations and chat icons.
|
||||
- `CustomLoader` - Main animated loader component
|
||||
- `ProgressLoader` - Loader with progress indicator
|
||||
- Predefined loader configurations for different scenarios
|
||||
- Smooth animations and transitions
|
||||
|
||||
## Usage
|
||||
|
||||
Import the utilities you need in your components:
|
||||
|
||||
```typescript
|
||||
import { showLocalNotification, createNotificationChannel } from './utilities/notificationUtils';
|
||||
import { requestNotificationPermission, checkNotificationPermission } from './utilities/permissionUtils';
|
||||
import { createDeepLinkHandler } from './utilities/deepLinkUtils';
|
||||
import { createWebViewHandler } from './utilities/webViewUtils';
|
||||
import { createCookieHandler } from './utilities/cookieUtils';
|
||||
import { createFCMHandler } from './utilities/fcmUtils';
|
||||
import { ALLOWED_DOMAIN, NAVIGATION_DELAYS, URL_PATTERNS } from './utilities/constants';
|
||||
import { CustomLoader, createOdooLoader, createChatLoader } from './utilities/loaderUtils';
|
||||
```
|
||||
|
||||
## Classes and Methods
|
||||
|
||||
### Deep Link Handler
|
||||
- `handleKilledAppDeepLink(data)` - Handle deep links from killed app
|
||||
- `handleForegroundBackgroundDeepLink(data)` - Handle deep links from foreground/background
|
||||
- `navigateToChannel(channelId)` - Navigate to specific channel
|
||||
- `handleDeepLink(deepLink)` - Process deep link URLs
|
||||
|
||||
### WebView Handler
|
||||
- `handleNavigation(request)` - Handle WebView navigation requests
|
||||
- `handleWebViewMessage(event, setWebViewCookies)` - Process WebView messages
|
||||
- `handleWebViewLoadEnd(currentChannel, isNavigating)` - Handle load completion
|
||||
- `injectDebugScript()` - Inject debug script into WebView
|
||||
|
||||
### Cookie Handler
|
||||
- `getWebViewCookies()` - Get authentication cookies
|
||||
- `refreshWebViewCookies()` - Set up periodic cookie refresh
|
||||
- `getCookiesForUrl(url)` - Get cookies for specific URL
|
||||
- `setCookiesForUrl(url, cookies)` - Set cookies for specific URL
|
||||
- `clearAllCookies()` - Clear all cookies
|
||||
- `isValidSessionCookie(cookies)` - Validate session cookies
|
||||
|
||||
### FCM Handler
|
||||
- `initializeNotifications()` - Initialize FCM notifications
|
||||
- `requestUserPermission()` - Request notification permissions
|
||||
- `getFcmToken()` - Get FCM token
|
||||
- `sendTokenToBackend(token)` - Send token to backend
|
||||
- `setupMessageListeners(appState)` - Setup FCM message listeners
|
||||
|
||||
### Constants
|
||||
- `ALLOWED_DOMAIN` - Base domain for the application
|
||||
- `NAVIGATION_DELAYS` - Timing constants for navigation delays
|
||||
- `URL_PATTERNS` - URL pattern templates
|
||||
- `FCM_REGISTER_ENDPOINT` - FCM registration endpoint
|
||||
- `COOKIE_REFRESH_INTERVAL` - Cookie refresh timing
|
||||
- `NOTIFICATION_CONFIG` - Notification channel configuration
|
||||
|
||||
### Loader Components
|
||||
- `CustomLoader` - Main animated loader with chat icon
|
||||
- `ProgressLoader` - Loader with progress indicator
|
||||
- `createOdooLoader()` - Pre-configured Odoo loader
|
||||
- `createChatLoader()` - Pre-configured chat loader
|
||||
- `createNavigationLoader()` - Pre-configured navigation loader
|
||||
- `LOADER_CONFIGS` - Predefined loader configurations
|
||||
|
||||
### Notification Utils
|
||||
- `createNotificationChannel()` - Creates Android notification channel
|
||||
- `configureNotificationSettings()` - Configures notification settings
|
||||
- `showLocalNotification(options)` - Shows local notification
|
||||
- `shouldShowCustomNotification(appState)` - Checks if custom notification should be shown
|
||||
- `testNotification()` - Test notification function
|
||||
|
||||
### Permission Utils
|
||||
- `requestNotificationPermission()` - Requests notification permission
|
||||
- `checkNotificationPermission()` - Checks current permission status
|
||||
- `needsNotificationPermission()` - Checks if permission is needed
|
||||
- `showPermissionExplanation()` - Shows permission explanation dialog
|
||||
- `showSettingsDialog()` - Shows settings dialog for denied permissions
|
||||
|
||||
## Architecture Benefits
|
||||
|
||||
1. **Separation of Concerns**: Each utility handles a specific domain
|
||||
2. **Reusability**: Utilities can be easily reused across components
|
||||
3. **Maintainability**: Code is organized and easy to understand
|
||||
4. **Testability**: Each utility can be tested independently
|
||||
5. **Type Safety**: Proper TypeScript interfaces and types
|
||||
6. **Documentation**: Comprehensive JSDoc comments throughout
|
||||
45
utilities/constants.ts
Normal file
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Common Constants
|
||||
* Centralized configuration constants for the T4B Chat application
|
||||
*/
|
||||
|
||||
// Domain Configuration
|
||||
export const ALLOWED_DOMAIN = 'http://192.168.1.12:8069';
|
||||
|
||||
// FCM Configuration
|
||||
export const FCM_REGISTER_ENDPOINT = '/fcm/register';
|
||||
|
||||
// Cookie Configuration
|
||||
export const COOKIE_REFRESH_INTERVAL = 30000; // 30 seconds
|
||||
export const INITIAL_COOKIE_DELAY = 10000; // 10 seconds
|
||||
|
||||
// Navigation Configuration
|
||||
export const NAVIGATION_DELAYS = {
|
||||
FOREGROUND: 100, // Minimal delay for foreground notifications
|
||||
BACKGROUND: 200, // Minimal delay for background notifications
|
||||
KILLED_APP: 100, // Minimal delay for killed app notifications
|
||||
TWO_STEP_NAVIGATION: 300, // Delay between two-step navigation
|
||||
NAVIGATION_RESET: 500, // Delay before resetting navigation state
|
||||
KILLED_APP_RESET: 800, // Delay before resetting killed app navigation state
|
||||
} as const;
|
||||
|
||||
// URL Patterns
|
||||
export const URL_PATTERNS = {
|
||||
MAIL_APP: '#action=106&cids=1&menu_id=73',
|
||||
CHAT_CHANNEL: (channelId: string) => `&active_id=mail.channel_${channelId}`,
|
||||
} as const;
|
||||
|
||||
// Deep Link Configuration
|
||||
export const DEEP_LINK_PREFIX = 'myapp://chat/';
|
||||
|
||||
// Platform Configuration
|
||||
export const PLATFORM_CONFIG = {
|
||||
ANDROID_MIN_VERSION_FOR_PERMISSIONS: 33, // Android 13+
|
||||
} as const;
|
||||
|
||||
// Notification Configuration
|
||||
export const NOTIFICATION_CONFIG = {
|
||||
CHANNEL_ID: 't4b_chat_channel',
|
||||
CHANNEL_NAME: 'T4B Chat Notifications',
|
||||
CHANNEL_DESCRIPTION: 'Notifications for T4B Chat messages',
|
||||
} as const;
|
||||
247
utilities/cookieUtils.ts
Normal file
@ -0,0 +1,247 @@
|
||||
/**
|
||||
* Cookie Utilities
|
||||
* Handles cookie management for WebView authentication
|
||||
*/
|
||||
|
||||
import CookieManager from '@react-native-cookies/cookies';
|
||||
import { Platform } from 'react-native';
|
||||
import { ALLOWED_DOMAIN, COOKIE_REFRESH_INTERVAL, INITIAL_COOKIE_DELAY } from './constants';
|
||||
|
||||
/**
|
||||
* Interface for cookie data
|
||||
*/
|
||||
export interface CookieData {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cookie Handler Class
|
||||
*/
|
||||
export class CookieHandler {
|
||||
private setWebViewCookies: (cookies: string | null) => void;
|
||||
|
||||
constructor(setWebViewCookies: (cookies: string | null) => void) {
|
||||
this.setWebViewCookies = setWebViewCookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WebView cookies for authentication
|
||||
* @returns Promise<string | null> - Cookie string or null if not found
|
||||
*/
|
||||
async getWebViewCookies(): Promise<string | null> {
|
||||
try {
|
||||
console.log('🍪 Requesting cookies using @react-native-cookies/cookies...');
|
||||
|
||||
const baseUrl = `${ALLOWED_DOMAIN}`;
|
||||
console.log('🍪 Getting cookies for URL:', baseUrl);
|
||||
|
||||
let cookieString = '';
|
||||
|
||||
// Platform-specific cookie handling
|
||||
if (Platform.OS === 'ios') {
|
||||
// iOS: Can use getAll()
|
||||
try {
|
||||
const cookies = await CookieManager.getAll();
|
||||
console.log('🍪 iOS - All cookies retrieved:', cookies);
|
||||
|
||||
// Filter cookies for our domain
|
||||
const domainCookies = Object.keys(cookies).filter(key =>
|
||||
key.includes(baseUrl) || key.includes(ALLOWED_DOMAIN.replace('/', ''))
|
||||
);
|
||||
|
||||
console.log('🍪 iOS - Domain cookies found:', domainCookies);
|
||||
|
||||
for (const cookieKey of domainCookies) {
|
||||
const cookie = cookies[cookieKey];
|
||||
console.log('🍪 iOS - Checking cookie:', cookieKey, cookie);
|
||||
|
||||
if (cookie && typeof cookie === 'object') {
|
||||
const cookieValue = cookie.value || cookie;
|
||||
if (cookieValue && typeof cookieValue === 'string' && cookieValue.includes('session_id=')) {
|
||||
const sessionMatch = cookieValue.match(/session_id=([^;]+)/);
|
||||
if (sessionMatch) {
|
||||
console.log('✅ iOS - Found session_id in cookie:', sessionMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof cookie === 'object' && cookie.name && cookie.value) {
|
||||
cookieString += `${cookie.name}=${cookie.value}; `;
|
||||
} else if (typeof cookie === 'string') {
|
||||
cookieString += cookie + '; ';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (iosError) {
|
||||
console.log('⚠️ iOS getAll() failed, trying direct method:', iosError);
|
||||
}
|
||||
}
|
||||
|
||||
// Both platforms: Try getting cookies directly for the domain
|
||||
try {
|
||||
console.log('🍪 Getting direct cookies for domain:', baseUrl);
|
||||
const directCookies = await CookieManager.get(baseUrl);
|
||||
console.log('🍪 Direct cookies for domain:', directCookies);
|
||||
|
||||
if (directCookies && Object.keys(directCookies).length > 0) {
|
||||
// Convert to cookie string format
|
||||
const directCookieString = Object.entries(directCookies)
|
||||
.map(([name, value]) => `${name}=${value}`)
|
||||
.join('; ');
|
||||
|
||||
console.log('🍪 Direct cookie string:', directCookieString);
|
||||
|
||||
if (directCookieString && directCookieString.includes('session_id')) {
|
||||
cookieString = directCookieString;
|
||||
console.log('✅ Found session_id in direct cookies:', cookieString);
|
||||
} else {
|
||||
console.warn('⚠️ Direct cookies found but no session_id:', directCookieString);
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ No direct cookies found for domain');
|
||||
}
|
||||
} catch (directError) {
|
||||
console.log('⚠️ Could not get direct cookies:', directError);
|
||||
}
|
||||
|
||||
// Fallback: Try getting cookies for the WebView URL
|
||||
try {
|
||||
const webViewUrl = `${ALLOWED_DOMAIN}/web`;
|
||||
console.log('🍪 Fallback - Getting cookies for WebView URL:', webViewUrl);
|
||||
const webViewCookies = await CookieManager.get(webViewUrl);
|
||||
console.log('🍪 WebView cookies:', webViewCookies);
|
||||
|
||||
if (webViewCookies && Object.keys(webViewCookies).length > 0) {
|
||||
const webViewCookieString = Object.entries(webViewCookies)
|
||||
.map(([name, value]) => `${name}=${value}`)
|
||||
.join('; ');
|
||||
|
||||
if (webViewCookieString && webViewCookieString.includes('session_id')) {
|
||||
cookieString = webViewCookieString;
|
||||
console.log('✅ Found session_id in WebView cookies:', cookieString);
|
||||
}
|
||||
}
|
||||
} catch (webViewError) {
|
||||
console.log('⚠️ Could not get WebView cookies:', webViewError);
|
||||
}
|
||||
|
||||
if (cookieString && cookieString.includes('session_id')) {
|
||||
console.log('✅ Valid session cookies found:', cookieString);
|
||||
this.setWebViewCookies(cookieString);
|
||||
return cookieString;
|
||||
} else {
|
||||
console.warn('⚠️ No session_id found in any cookie method');
|
||||
console.warn('⚠️ Final cookie string:', cookieString);
|
||||
this.setWebViewCookies(null);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error getting cookies with @react-native-cookies/cookies:', error);
|
||||
this.setWebViewCookies(null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh WebView cookies periodically
|
||||
* @returns Function to clear the interval
|
||||
*/
|
||||
refreshWebViewCookies(): () => void {
|
||||
// Initial cookie request after a longer delay to ensure WebView is fully loaded and user is logged in
|
||||
setTimeout(() => {
|
||||
console.log('🔄 Initial cookie refresh after WebView load');
|
||||
this.getWebViewCookies();
|
||||
}, INITIAL_COOKIE_DELAY);
|
||||
|
||||
// Set up periodic cookie refresh
|
||||
const cookieRefreshInterval = setInterval(() => {
|
||||
console.log('🔄 Periodic cookie refresh');
|
||||
this.getWebViewCookies();
|
||||
}, COOKIE_REFRESH_INTERVAL);
|
||||
|
||||
// Return cleanup function
|
||||
return () => clearInterval(cookieRefreshInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cookies for a specific URL
|
||||
* @param url - URL to get cookies for
|
||||
* @returns Promise<CookieData | null> - Cookie data or null
|
||||
*/
|
||||
async getCookiesForUrl(url: string): Promise<CookieData | null> {
|
||||
try {
|
||||
console.log('🍪 Getting cookies for URL:', url);
|
||||
const cookies = await CookieManager.get(url);
|
||||
console.log('🍪 Cookies for URL:', cookies);
|
||||
return cookies;
|
||||
} catch (error) {
|
||||
console.error('❌ Error getting cookies for URL:', url, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cookies for a specific URL
|
||||
* @param url - URL to set cookies for
|
||||
* @param cookies - Cookie data to set
|
||||
* @returns Promise<boolean> - Success status
|
||||
*/
|
||||
async setCookiesForUrl(url: string, cookies: any): Promise<boolean> {
|
||||
try {
|
||||
console.log('🍪 Setting cookies for URL:', url, cookies);
|
||||
await CookieManager.set(url, cookies);
|
||||
console.log('✅ Cookies set successfully for URL:', url);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Error setting cookies for URL:', url, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cookies
|
||||
* @returns Promise<boolean> - Success status
|
||||
*/
|
||||
async clearAllCookies(): Promise<boolean> {
|
||||
try {
|
||||
console.log('🍪 Clearing all cookies');
|
||||
await CookieManager.clearAll();
|
||||
console.log('✅ All cookies cleared successfully');
|
||||
this.setWebViewCookies(null);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Error clearing cookies:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if session cookies are valid
|
||||
* @param cookies - Cookie string to validate
|
||||
* @returns boolean - Whether cookies are valid
|
||||
*/
|
||||
isValidSessionCookie(cookies: string | null): boolean {
|
||||
if (!cookies) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasSessionId = cookies.includes('session_id=');
|
||||
const hasValidFormat = cookies.includes('=') && cookies.includes(';');
|
||||
|
||||
console.log('🍪 Cookie validation:', {
|
||||
hasSessionId,
|
||||
hasValidFormat,
|
||||
cookieLength: cookies.length
|
||||
});
|
||||
|
||||
return hasSessionId && hasValidFormat;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create cookie handler instance
|
||||
*/
|
||||
export function createCookieHandler(
|
||||
setWebViewCookies: (cookies: string | null) => void
|
||||
): CookieHandler {
|
||||
return new CookieHandler(setWebViewCookies);
|
||||
}
|
||||
242
utilities/deepLinkUtils.ts
Normal file
@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Deep Link Utilities
|
||||
* Handles deep linking functionality for FCM notifications and chat navigation
|
||||
*/
|
||||
|
||||
import { Platform } from 'react-native';
|
||||
import {
|
||||
ALLOWED_DOMAIN,
|
||||
NAVIGATION_DELAYS,
|
||||
URL_PATTERNS,
|
||||
DEEP_LINK_PREFIX
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* Interface for notification data
|
||||
*/
|
||||
export interface NotificationData {
|
||||
channel_id?: string;
|
||||
conversation_id?: string;
|
||||
deep_link?: string;
|
||||
title?: string;
|
||||
body?: string;
|
||||
channel_name?: string;
|
||||
author_name?: string;
|
||||
notification_type?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for navigation state
|
||||
*/
|
||||
export interface NavigationState {
|
||||
currentChannel: string | null;
|
||||
isNavigating: boolean;
|
||||
navigationAttempts: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep Link Handler Class
|
||||
*/
|
||||
export class DeepLinkHandler {
|
||||
private updateWebViewUrl: (url: string) => void;
|
||||
private setIsNavigating: (navigating: boolean) => void;
|
||||
private setCurrentChannel: (channel: string | null) => void;
|
||||
private setNavigationAttempts: (attempts: number) => void;
|
||||
private navigationState: NavigationState;
|
||||
|
||||
constructor(
|
||||
updateWebViewUrl: (url: string) => void,
|
||||
setIsNavigating: (navigating: boolean) => void,
|
||||
setCurrentChannel: (channel: string | null) => void,
|
||||
setNavigationAttempts: (attempts: number) => void,
|
||||
navigationState: NavigationState
|
||||
) {
|
||||
this.updateWebViewUrl = updateWebViewUrl;
|
||||
this.setIsNavigating = setIsNavigating;
|
||||
this.setCurrentChannel = setCurrentChannel;
|
||||
this.setNavigationAttempts = setNavigationAttempts;
|
||||
this.navigationState = navigationState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle deep linking from notifications (killed app case)
|
||||
* @param data - Notification data containing channel information
|
||||
*/
|
||||
handleKilledAppDeepLink(data: NotificationData): void {
|
||||
console.log('🔗 Processing deep link from notification (killed app case):', data);
|
||||
|
||||
const channelId = this.extractChannelId(data);
|
||||
if (!channelId) {
|
||||
console.warn('⚠️ No channel ID found in deep link data:', data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Minimal delay for killed app initialization
|
||||
setTimeout(() => {
|
||||
console.log('🔗 Setting initial WebView URL for killed app navigation to channel:', channelId);
|
||||
|
||||
// Set navigation state
|
||||
this.setIsNavigating(true);
|
||||
this.setCurrentChannel(channelId);
|
||||
|
||||
// Construct and set the target URL directly (works better for killed app)
|
||||
const chatUrl = this.buildChatUrl(channelId);
|
||||
console.log('🔗 Initial chat URL for killed app:', chatUrl);
|
||||
this.updateWebViewUrl(chatUrl);
|
||||
|
||||
// Reset navigation state after shorter delay
|
||||
setTimeout(() => {
|
||||
this.setIsNavigating(false);
|
||||
}, NAVIGATION_DELAYS.KILLED_APP_RESET);
|
||||
}, NAVIGATION_DELAYS.KILLED_APP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle WebView URL update from notification (foreground/background)
|
||||
* @param data - Notification data containing channel information
|
||||
*/
|
||||
handleForegroundBackgroundDeepLink(data: NotificationData): void {
|
||||
console.log('🔗 Updating WebView URL from notification:', data);
|
||||
|
||||
const channelId = this.extractChannelId(data);
|
||||
if (!channelId) {
|
||||
console.warn('⚠️ No channel ID found in notification data:', data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're already navigating to this channel
|
||||
if (this.navigationState.isNavigating && this.navigationState.currentChannel === channelId) {
|
||||
console.log('🔗 Already navigating to channel:', channelId);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔗 Navigating to channel via WebView URL update:', channelId);
|
||||
|
||||
// Set navigation state to prevent loops
|
||||
this.setIsNavigating(true);
|
||||
this.setNavigationAttempts(0);
|
||||
this.setCurrentChannel(channelId);
|
||||
|
||||
// Step 1: First navigate to mail app (without specific channel)
|
||||
const mailAppUrl = `${ALLOWED_DOMAIN}/web${URL_PATTERNS.MAIL_APP}`;
|
||||
console.log('🔗 Step 1: Navigating to mail app:', mailAppUrl);
|
||||
this.updateWebViewUrl(mailAppUrl);
|
||||
|
||||
// Step 2: After a minimal delay, navigate to specific channel
|
||||
setTimeout(() => {
|
||||
const chatUrl = this.buildChatUrl(channelId);
|
||||
console.log('🔗 Step 2: Navigating to specific channel:', chatUrl);
|
||||
this.updateWebViewUrl(chatUrl);
|
||||
|
||||
// Reset navigation state after final navigation
|
||||
setTimeout(() => {
|
||||
this.setIsNavigating(false);
|
||||
}, NAVIGATION_DELAYS.NAVIGATION_RESET);
|
||||
}, NAVIGATION_DELAYS.TWO_STEP_NAVIGATION);
|
||||
|
||||
console.log('✅ WebView navigation sequence started');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to specific channel (direct navigation for killed app)
|
||||
* @param channelId - Channel ID to navigate to
|
||||
*/
|
||||
navigateToChannel(channelId: string): void {
|
||||
if (!channelId) {
|
||||
console.warn('⚠️ No channel ID provided for navigation');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're already navigating to this channel
|
||||
if (this.navigationState.isNavigating && this.navigationState.currentChannel === channelId) {
|
||||
console.log('🔗 Already navigating to channel:', channelId);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔗 Navigating to channel (killed app - direct navigation):', channelId);
|
||||
this.setCurrentChannel(channelId);
|
||||
this.setIsNavigating(true);
|
||||
this.setNavigationAttempts(0);
|
||||
|
||||
// For killed app case, direct navigation works better
|
||||
const chatUrl = this.buildChatUrl(channelId);
|
||||
console.log('🔗 Chat URL (direct):', chatUrl);
|
||||
|
||||
// Update WebView URL directly - this works well for killed app
|
||||
this.updateWebViewUrl(chatUrl);
|
||||
|
||||
console.log('✅ WebView URL updated (direct navigation)');
|
||||
|
||||
// Reset navigation state after delay
|
||||
setTimeout(() => {
|
||||
this.setIsNavigating(false);
|
||||
}, NAVIGATION_DELAYS.KILLED_APP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle deep link URLs
|
||||
* @param deepLink - Deep link URL to process
|
||||
*/
|
||||
handleDeepLink(deepLink: string): void {
|
||||
console.log('🔗 Processing deep link:', deepLink);
|
||||
|
||||
// Parse deep link: myapp://chat/{channel_id}
|
||||
if (deepLink.startsWith(DEEP_LINK_PREFIX)) {
|
||||
const channelId = deepLink.replace(DEEP_LINK_PREFIX, '');
|
||||
console.log('🔗 Extracted channel ID:', channelId);
|
||||
this.navigateToChannel(channelId);
|
||||
} else {
|
||||
console.warn('⚠️ Unknown deep link format:', deepLink);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract channel ID from notification data
|
||||
* @param data - Notification data
|
||||
* @returns Channel ID or null if not found
|
||||
*/
|
||||
private extractChannelId(data: NotificationData): string | null {
|
||||
// Extract channel ID from different possible fields
|
||||
if (data.channel_id) {
|
||||
console.log('🔗 Found channel_id:', data.channel_id);
|
||||
return data.channel_id;
|
||||
} else if (data.conversation_id) {
|
||||
console.log('🔗 Found conversation_id:', data.conversation_id);
|
||||
return data.conversation_id;
|
||||
} else if (data.deep_link && data.deep_link.includes('chat/')) {
|
||||
const channelId = data.deep_link.replace(DEEP_LINK_PREFIX, '');
|
||||
console.log('🔗 Extracted channel_id from deep_link:', channelId);
|
||||
return channelId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build chat URL for specific channel
|
||||
* @param channelId - Channel ID
|
||||
* @returns Complete chat URL
|
||||
*/
|
||||
private buildChatUrl(channelId: string): string {
|
||||
return `${ALLOWED_DOMAIN}/web${URL_PATTERNS.MAIL_APP}${URL_PATTERNS.CHAT_CHANNEL(channelId)}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create deep link handler instance
|
||||
*/
|
||||
export function createDeepLinkHandler(
|
||||
updateWebViewUrl: (url: string) => void,
|
||||
setIsNavigating: (navigating: boolean) => void,
|
||||
setCurrentChannel: (channel: string | null) => void,
|
||||
setNavigationAttempts: (attempts: number) => void,
|
||||
navigationState: NavigationState
|
||||
): DeepLinkHandler {
|
||||
return new DeepLinkHandler(
|
||||
updateWebViewUrl,
|
||||
setIsNavigating,
|
||||
setCurrentChannel,
|
||||
setNavigationAttempts,
|
||||
navigationState
|
||||
);
|
||||
}
|
||||
357
utilities/fcmUtils.ts
Normal file
@ -0,0 +1,357 @@
|
||||
/**
|
||||
* FCM (Firebase Cloud Messaging) Utilities
|
||||
* Handles FCM token management, messaging, and backend communication
|
||||
*/
|
||||
|
||||
import messaging, { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
|
||||
import { Platform } from 'react-native';
|
||||
import {
|
||||
requestNotificationPermission,
|
||||
checkNotificationPermission,
|
||||
needsNotificationPermission,
|
||||
showPermissionExplanation,
|
||||
showSettingsDialog,
|
||||
PermissionResult
|
||||
} from './permissionUtils';
|
||||
import { RESULTS } from 'react-native-permissions';
|
||||
import { showLocalNotification } from './notificationUtils';
|
||||
import { ALLOWED_DOMAIN, FCM_REGISTER_ENDPOINT, NAVIGATION_DELAYS } from './constants';
|
||||
|
||||
/**
|
||||
* Interface for FCM token data
|
||||
*/
|
||||
export interface FCMTokenData {
|
||||
token: string;
|
||||
deviceName: string;
|
||||
deviceType: string;
|
||||
appVersion: string;
|
||||
osVersion: string;
|
||||
sessionCookies: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for notification callback functions
|
||||
*/
|
||||
export interface NotificationCallbacks {
|
||||
onForegroundNotification: (data: any) => void;
|
||||
onBackgroundNotification: (data: any) => void;
|
||||
onKilledAppNotification: (data: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* FCM Handler Class
|
||||
*/
|
||||
export class FCMHandler {
|
||||
private getWebViewCookies: () => Promise<string | null>;
|
||||
private callbacks: NotificationCallbacks;
|
||||
|
||||
constructor(
|
||||
getWebViewCookies: () => Promise<string | null>,
|
||||
callbacks: NotificationCallbacks
|
||||
) {
|
||||
this.getWebViewCookies = getWebViewCookies;
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize FCM notifications
|
||||
*/
|
||||
async initializeNotifications(): Promise<void> {
|
||||
try {
|
||||
console.log('Initializing notifications...');
|
||||
|
||||
// Request permission first
|
||||
await this.requestUserPermission();
|
||||
|
||||
// Then get FCM token
|
||||
await this.getFcmToken();
|
||||
|
||||
console.log('Notification initialization completed');
|
||||
} catch (error) {
|
||||
console.error('Error initializing notifications:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request user permission for notifications
|
||||
*/
|
||||
async requestUserPermission(): Promise<void> {
|
||||
try {
|
||||
console.log('Checking notification permission requirements...');
|
||||
|
||||
// Check if permission is needed based on platform and version
|
||||
const needsPermission = needsNotificationPermission();
|
||||
console.log('Permission needed:', needsPermission);
|
||||
|
||||
if (!needsPermission) {
|
||||
console.log('No explicit permission required for this Android version');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check current permission status first
|
||||
const currentStatus = await checkNotificationPermission();
|
||||
console.log('Current permission status:', currentStatus);
|
||||
|
||||
if (currentStatus.granted) {
|
||||
console.log('Notification permission already granted');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show explanation dialog for Android users
|
||||
if (Platform.OS === 'android') {
|
||||
const userWantsPermission = await showPermissionExplanation();
|
||||
if (!userWantsPermission) {
|
||||
console.log('User declined permission request');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Request permission
|
||||
const result: PermissionResult = await requestNotificationPermission();
|
||||
console.log('📋 Permission request result:', JSON.stringify(result, null, 2));
|
||||
|
||||
if (result.granted) {
|
||||
console.log('✅ Notification permission granted successfully');
|
||||
} else {
|
||||
console.log('❌ Notification permission denied');
|
||||
console.log('📋 Denial details:', {
|
||||
status: result.status,
|
||||
platform: result.platform,
|
||||
androidVersion: result.androidVersion
|
||||
});
|
||||
|
||||
// Check for different denial scenarios
|
||||
if (Platform.OS === 'android') {
|
||||
if (result.status === RESULTS.DENIED) {
|
||||
console.log('🚫 Permission denied by user');
|
||||
showSettingsDialog();
|
||||
} else if (result.status === RESULTS.BLOCKED) {
|
||||
console.log('🚫 Permission blocked by user');
|
||||
showSettingsDialog();
|
||||
} else if (result.status === RESULTS.UNAVAILABLE) {
|
||||
console.log('⚠️ Permission not available on this device');
|
||||
} else {
|
||||
console.log('❓ Unknown permission status:', result.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error requesting permission:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get FCM token
|
||||
*/
|
||||
async getFcmToken(): Promise<void> {
|
||||
try {
|
||||
console.log('🔑 Attempting to get FCM token...');
|
||||
|
||||
// Check if we have permission before trying to get token
|
||||
const permissionStatus = await checkNotificationPermission();
|
||||
console.log('🔍 Permission status for token:', JSON.stringify(permissionStatus, null, 2));
|
||||
|
||||
if (!permissionStatus.granted && needsNotificationPermission()) {
|
||||
console.log('❌ Cannot get FCM token: notification permission not granted');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Permission check passed, requesting FCM token...');
|
||||
const token = await messaging().getToken();
|
||||
console.log('🎫 FCM Token received:', token ? 'YES' : 'NO');
|
||||
|
||||
if (token) {
|
||||
console.log('📤 FCM Token:', token);
|
||||
// Send FCM token to Odoo backend
|
||||
await this.sendTokenToBackend(token);
|
||||
} else {
|
||||
console.log('⚠️ No FCM token available');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Error getting FCM token:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send FCM token to backend
|
||||
* @param token - FCM token to send
|
||||
*/
|
||||
async sendTokenToBackend(token: string): Promise<void> {
|
||||
try {
|
||||
// Import react-native-device-info
|
||||
const DeviceInfo = require('react-native-device-info');
|
||||
|
||||
// Get device information
|
||||
const deviceName = await DeviceInfo.getDeviceName();
|
||||
const deviceType = Platform.OS;
|
||||
const appVersion = await DeviceInfo.getVersion();
|
||||
const osVersion = await DeviceInfo.getSystemVersion();
|
||||
|
||||
// Get session cookies from WebView
|
||||
const sessionCookies = await this.getWebViewCookies();
|
||||
|
||||
console.log('📤 Sending FCM token to backend:', {
|
||||
token: token ? token.substring(0, 20) + '...' : 'NO',
|
||||
deviceName,
|
||||
deviceType,
|
||||
appVersion,
|
||||
osVersion,
|
||||
hasSessionCookies: !!sessionCookies
|
||||
});
|
||||
|
||||
if (!sessionCookies) {
|
||||
console.warn('⚠️ No session cookies found - user may not be logged in');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`${ALLOWED_DOMAIN}${FCM_REGISTER_ENDPOINT}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cookie': sessionCookies,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: token,
|
||||
device_name: deviceName,
|
||||
device_type: deviceType,
|
||||
app_version: appVersion,
|
||||
os_version: osVersion,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
console.log('✅ FCM token registered successfully:', result);
|
||||
} else {
|
||||
console.error('❌ Failed to register FCM token:', response.status, response.statusText);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Error sending token to backend:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle FCM messages when app is in foreground
|
||||
* @param remoteMessage - FCM message received
|
||||
* @param appState - Current app state
|
||||
*/
|
||||
handleForegroundMessage(
|
||||
remoteMessage: FirebaseMessagingTypes.RemoteMessage,
|
||||
appState: string
|
||||
): void {
|
||||
console.log('📨 FCM Message received:', JSON.stringify(remoteMessage, null, 2));
|
||||
console.log('📱 Current app state:', appState);
|
||||
|
||||
// Always show notification in foreground - FCM doesn't show notifications automatically in foreground
|
||||
console.log('📤 Showing foreground notification');
|
||||
|
||||
if (remoteMessage.notification) {
|
||||
console.log('📤 Showing notification with payload');
|
||||
showLocalNotification({
|
||||
title: remoteMessage.notification.title || 'New Message',
|
||||
message: remoteMessage.notification.body || 'You have a new message',
|
||||
});
|
||||
|
||||
// For foreground: Update WebView URL directly based on notification data
|
||||
if (remoteMessage.data) {
|
||||
console.log('🔗 Processing notification data for WebView URL update (foreground):', remoteMessage.data);
|
||||
setTimeout(() => {
|
||||
this.callbacks.onForegroundNotification(remoteMessage.data);
|
||||
}, NAVIGATION_DELAYS.FOREGROUND);
|
||||
}
|
||||
} else if (remoteMessage.data) {
|
||||
console.log('📤 Showing notification with data only');
|
||||
|
||||
// Extract notification info from data payload
|
||||
const title = String(remoteMessage.data.title || 'Odoo Chat');
|
||||
const body = String(remoteMessage.data.body || 'New message received');
|
||||
|
||||
showLocalNotification({
|
||||
title: title,
|
||||
message: body,
|
||||
});
|
||||
|
||||
// For foreground: Update WebView URL directly based on notification data
|
||||
console.log('🔗 Processing data payload for WebView URL update (foreground):', remoteMessage.data);
|
||||
setTimeout(() => {
|
||||
this.callbacks.onForegroundNotification(remoteMessage.data);
|
||||
}, NAVIGATION_DELAYS.FOREGROUND);
|
||||
} else {
|
||||
console.log('⚠️ No notification content available');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle FCM messages when app is opened from background
|
||||
* @param remoteMessage - FCM message received
|
||||
*/
|
||||
handleBackgroundMessage(remoteMessage: FirebaseMessagingTypes.RemoteMessage): void {
|
||||
if (remoteMessage?.data) {
|
||||
console.log('🔗 App opened from background with notification (using WebView URL update):', remoteMessage.data);
|
||||
// For background state: Update WebView URL directly
|
||||
setTimeout(() => {
|
||||
this.callbacks.onBackgroundNotification(remoteMessage.data);
|
||||
}, NAVIGATION_DELAYS.BACKGROUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle FCM messages when app is opened from quit state
|
||||
* @param remoteMessage - FCM message received
|
||||
*/
|
||||
handleKilledAppMessage(remoteMessage: FirebaseMessagingTypes.RemoteMessage): void {
|
||||
if (remoteMessage?.data) {
|
||||
console.log('🔗 App opened from quit state with notification (using deep linking):', remoteMessage.data);
|
||||
// For quit state: Use proper deep linking approach
|
||||
setTimeout(() => {
|
||||
this.callbacks.onKilledAppNotification(remoteMessage.data);
|
||||
}, NAVIGATION_DELAYS.KILLED_APP);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup FCM message listeners
|
||||
* @param appState - Current app state
|
||||
* @returns Function to unsubscribe from listeners
|
||||
*/
|
||||
setupMessageListeners(appState: string): () => void {
|
||||
// Handle FCM messages when app is in foreground
|
||||
const unsubscribe = messaging().onMessage(async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
|
||||
this.handleForegroundMessage(remoteMessage, appState);
|
||||
});
|
||||
|
||||
// Handle background/quit state notifications with deep linking
|
||||
messaging()
|
||||
.getInitialNotification()
|
||||
.then((remoteMessage: FirebaseMessagingTypes.RemoteMessage | null) => {
|
||||
if (remoteMessage) {
|
||||
this.handleKilledAppMessage(remoteMessage);
|
||||
}
|
||||
});
|
||||
|
||||
messaging().onNotificationOpenedApp((remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
|
||||
this.handleBackgroundMessage(remoteMessage);
|
||||
});
|
||||
|
||||
// Listen for token refresh
|
||||
const unsubscribeTokenRefresh = messaging().onTokenRefresh(async (token) => {
|
||||
console.log('🔄 FCM Token refreshed:', token);
|
||||
await this.sendTokenToBackend(token);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
unsubscribeTokenRefresh();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create FCM handler instance
|
||||
*/
|
||||
export function createFCMHandler(
|
||||
getWebViewCookies: () => Promise<string | null>,
|
||||
callbacks: NotificationCallbacks
|
||||
): FCMHandler {
|
||||
return new FCMHandler(getWebViewCookies, callbacks);
|
||||
}
|
||||
387
utilities/loaderUtils.tsx
Normal file
@ -0,0 +1,387 @@
|
||||
/**
|
||||
* Loader Utilities
|
||||
* Beautiful custom loading component with chat icon and animations
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
StyleSheet,
|
||||
Dimensions
|
||||
} from 'react-native';
|
||||
//@ts-ignore
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
// Get screen dimensions
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
/**
|
||||
* Interface for loader props
|
||||
*/
|
||||
export interface LoaderProps {
|
||||
isLoading: boolean;
|
||||
loadingText?: string;
|
||||
showChatIcon?: boolean;
|
||||
iconName?: string;
|
||||
iconSize?: number;
|
||||
backgroundColor?: string;
|
||||
primaryColor?: string;
|
||||
textColor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Beautiful Custom Loader Component
|
||||
*/
|
||||
export const CustomLoader: React.FC<LoaderProps> = ({
|
||||
isLoading,
|
||||
loadingText = 'Loading...',
|
||||
showChatIcon = true,
|
||||
iconName = 'chat-processing',
|
||||
iconSize = 40,
|
||||
backgroundColor = 'rgba(255, 255, 255, 1)', // Pure white
|
||||
primaryColor = '#007AFF',
|
||||
textColor = '#333333'
|
||||
}) => {
|
||||
// Animation values
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
const scaleAnim = useRef(new Animated.Value(0.8)).current;
|
||||
const pulseAnim = useRef(new Animated.Value(1)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
// Instant animations for better UX during navigation
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
duration: 0,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(scaleAnim, {
|
||||
toValue: 1,
|
||||
duration: 0,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
|
||||
// Pulse animation with reduced intensity
|
||||
const pulseAnimation = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1.05,
|
||||
duration: 800,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1,
|
||||
duration: 800,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
])
|
||||
);
|
||||
pulseAnimation.start();
|
||||
|
||||
return () => {
|
||||
pulseAnimation.stop();
|
||||
};
|
||||
} else {
|
||||
// Instant hide animations for better UX during navigation
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 0,
|
||||
duration: 0,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(scaleAnim, {
|
||||
toValue: 0.8,
|
||||
duration: 0,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
if (!isLoading) return null;
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.loaderContainer,
|
||||
{
|
||||
opacity: fadeAnim,
|
||||
transform: [{ scale: scaleAnim }]
|
||||
}
|
||||
]}
|
||||
>
|
||||
<View style={styles.loaderContent}>
|
||||
{/* Chat Icon */}
|
||||
{showChatIcon && (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.chatIconContainer,
|
||||
{
|
||||
backgroundColor: primaryColor,
|
||||
transform: [
|
||||
{ scale: pulseAnim }
|
||||
]
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Icon name={iconName} size={iconSize} color="white" />
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
{/* Loading Spinner */}
|
||||
<View style={styles.spinnerContainer}>
|
||||
<ActivityIndicator
|
||||
size="large"
|
||||
color={primaryColor}
|
||||
style={styles.spinner}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Loading Text */}
|
||||
<Animated.Text
|
||||
style={[
|
||||
styles.loadingText,
|
||||
{
|
||||
color: textColor,
|
||||
opacity: fadeAnim
|
||||
}
|
||||
]}
|
||||
>
|
||||
{loadingText}
|
||||
</Animated.Text>
|
||||
|
||||
{/* Loading Dots Animation */}
|
||||
<View style={styles.dotsContainer}>
|
||||
{[0, 1, 2].map((index) => (
|
||||
<Animated.View
|
||||
key={index}
|
||||
style={[
|
||||
styles.dot,
|
||||
{
|
||||
backgroundColor: primaryColor,
|
||||
opacity: pulseAnim,
|
||||
transform: [
|
||||
{
|
||||
scale: pulseAnim.interpolate({
|
||||
inputRange: [1, 1.1],
|
||||
outputRange: [1, 1.2],
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Enhanced Loader with Progress
|
||||
*/
|
||||
export const ProgressLoader: React.FC<LoaderProps & { progress?: number }> = ({
|
||||
isLoading,
|
||||
loadingText = 'Loading...',
|
||||
progress = 0,
|
||||
...props
|
||||
}) => {
|
||||
const progressAnim = useRef(new Animated.Value(0)).current;
|
||||
|
||||
useEffect(() => {
|
||||
Animated.timing(progressAnim, {
|
||||
toValue: progress,
|
||||
duration: 300,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
}, [progress]);
|
||||
|
||||
return (
|
||||
<CustomLoader
|
||||
isLoading={isLoading}
|
||||
loadingText={`${loadingText} ${Math.round(progress * 100)}%`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create loader styles
|
||||
*/
|
||||
const styles = StyleSheet.create({
|
||||
loaderContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 9999,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(255, 255, 255, 1)', // Pure white background
|
||||
},
|
||||
loaderContent: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 40,
|
||||
paddingVertical: 30,
|
||||
borderRadius: 20,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 4,
|
||||
},
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 4.65,
|
||||
elevation: 8,
|
||||
backgroundColor: 'rgba(255, 255, 255, 1)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
chatIconContainer: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
shadowColor: '#007AFF',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
},
|
||||
spinnerContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
spinner: {
|
||||
transform: [{ scale: 1.2 }],
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
marginBottom: 15,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
dotsContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginHorizontal: 4,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Predefined loader configurations with vector icons
|
||||
*/
|
||||
export const LOADER_CONFIGS = {
|
||||
CHAT: {
|
||||
loadingText: 'Connecting to chat...',
|
||||
showChatIcon: true,
|
||||
iconName: 'chat-processing' as const,
|
||||
iconSize: 40,
|
||||
primaryColor: '#007AFF',
|
||||
},
|
||||
ODOO: {
|
||||
loadingText: 'Loading T4B...',
|
||||
showChatIcon: true,
|
||||
iconName: 'chat-processing' as const,
|
||||
iconSize: 40,
|
||||
primaryColor: '#875A7B',
|
||||
},
|
||||
NAVIGATION: {
|
||||
loadingText: 'Navigating...',
|
||||
showChatIcon: true,
|
||||
iconName: 'navigation' as const,
|
||||
iconSize: 40,
|
||||
primaryColor: '#28A745',
|
||||
},
|
||||
MESSAGE: {
|
||||
loadingText: 'Sending message...',
|
||||
showChatIcon: true,
|
||||
iconName: 'message-text' as const,
|
||||
iconSize: 40,
|
||||
primaryColor: '#FF6B35',
|
||||
},
|
||||
SYNC: {
|
||||
loadingText: 'Synchronizing...',
|
||||
showChatIcon: true,
|
||||
iconName: 'sync' as const,
|
||||
iconSize: 40,
|
||||
primaryColor: '#9C27B0',
|
||||
},
|
||||
DEFAULT: {
|
||||
loadingText: 'Loading...',
|
||||
showChatIcon: true,
|
||||
iconName: 'loading' as const,
|
||||
iconSize: 40,
|
||||
primaryColor: '#007AFF',
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Quick loader functions with vector icons
|
||||
*/
|
||||
export const createChatLoader = (isLoading: boolean, text?: string) => (
|
||||
<CustomLoader
|
||||
isLoading={isLoading}
|
||||
{...LOADER_CONFIGS.CHAT}
|
||||
loadingText={text || LOADER_CONFIGS.CHAT.loadingText}
|
||||
/>
|
||||
);
|
||||
|
||||
export const createOdooLoader = (isLoading: boolean, text?: string) => (
|
||||
<CustomLoader
|
||||
isLoading={isLoading}
|
||||
{...LOADER_CONFIGS.ODOO}
|
||||
loadingText={text || LOADER_CONFIGS.ODOO.loadingText}
|
||||
/>
|
||||
);
|
||||
|
||||
export const createNavigationLoader = (isLoading: boolean, text?: string) => (
|
||||
<CustomLoader
|
||||
isLoading={isLoading}
|
||||
{...LOADER_CONFIGS.NAVIGATION}
|
||||
loadingText={text || LOADER_CONFIGS.NAVIGATION.loadingText}
|
||||
/>
|
||||
);
|
||||
|
||||
export const createMessageLoader = (isLoading: boolean, text?: string) => (
|
||||
<CustomLoader
|
||||
isLoading={isLoading}
|
||||
{...LOADER_CONFIGS.MESSAGE}
|
||||
loadingText={text || LOADER_CONFIGS.MESSAGE.loadingText}
|
||||
/>
|
||||
);
|
||||
|
||||
export const createSyncLoader = (isLoading: boolean, text?: string) => (
|
||||
<CustomLoader
|
||||
isLoading={isLoading}
|
||||
{...LOADER_CONFIGS.SYNC}
|
||||
loadingText={text || LOADER_CONFIGS.SYNC.loadingText}
|
||||
/>
|
||||
);
|
||||
|
||||
export const createDefaultLoader = (isLoading: boolean, text?: string) => (
|
||||
<CustomLoader
|
||||
isLoading={isLoading}
|
||||
{...LOADER_CONFIGS.DEFAULT}
|
||||
loadingText={text || LOADER_CONFIGS.DEFAULT.loadingText}
|
||||
/>
|
||||
);
|
||||
152
utilities/notificationUtils.ts
Normal file
@ -0,0 +1,152 @@
|
||||
// @ts-ignore
|
||||
import PushNotification from 'react-native-push-notification';
|
||||
import { AppState, AppStateStatus } from 'react-native';
|
||||
|
||||
/**
|
||||
* Creates a notification channel for Android
|
||||
*/
|
||||
export const createNotificationChannel = (): void => {
|
||||
PushNotification.createChannel(
|
||||
{
|
||||
channelId: 'default-channel-id',
|
||||
channelName: 'Default Channel',
|
||||
channelDescription: 'A default channel for notifications',
|
||||
playSound: true,
|
||||
soundName: 'default',
|
||||
importance: 4, // High importance
|
||||
vibrate: true,
|
||||
},
|
||||
(created: boolean) => console.log(`Notification channel created: ${created}`)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Configures notification settings
|
||||
*/
|
||||
export const configureNotificationSettings = (): void => {
|
||||
PushNotification.configure({
|
||||
// (optional) Called when Token is generated (iOS and Android)
|
||||
onRegister: function (token: any) {
|
||||
console.log('TOKEN:', token);
|
||||
},
|
||||
|
||||
// (required) Called when a remote is received or opened, or local notification is opened
|
||||
onNotification: function (notification: any) {
|
||||
console.log('NOTIFICATION:', notification);
|
||||
},
|
||||
|
||||
// (optional) Called when Registered Action is pressed and invokeApp is false, if true onNotification will be called (Android)
|
||||
onAction: function (notification: any) {
|
||||
console.log('ACTION:', notification.action);
|
||||
console.log('NOTIFICATION:', notification);
|
||||
},
|
||||
|
||||
// (optional) Called when the user fails to register for remote notifications. Typically occurs when APNS is having issues, or the device is a simulator. (iOS)
|
||||
onRegistrationError: function (err: any) {
|
||||
console.error(err.message, err);
|
||||
},
|
||||
|
||||
// IOS ONLY (optional): default: all - Permissions to register.
|
||||
permissions: {
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
},
|
||||
|
||||
// Should the initial notification be popped automatically
|
||||
// default: true
|
||||
popInitialNotification: true,
|
||||
|
||||
/**
|
||||
* (optional) default: true
|
||||
* - Specified if permissions (ios) and token (android and ios) will requested or not,
|
||||
* - if not, you must call PushNotificationsHandler.requestPermissions() later
|
||||
* - if you are not using remote notification or do not have Firebase installed, use this:
|
||||
* requestPermissions: Platform.OS === 'ios'
|
||||
*/
|
||||
requestPermissions: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a local notification
|
||||
*/
|
||||
export const showLocalNotification = (notificationData: {
|
||||
title: string;
|
||||
message: string;
|
||||
channelId?: string;
|
||||
playSound?: boolean;
|
||||
soundName?: string;
|
||||
vibrate?: boolean;
|
||||
priority?: 'min' | 'low' | 'default' | 'high' | 'max';
|
||||
visibility?: 'private' | 'public' | 'secret';
|
||||
}): void => {
|
||||
PushNotification.localNotification({
|
||||
channelId: notificationData.channelId || 'default-channel-id',
|
||||
title: notificationData.title,
|
||||
message: notificationData.message,
|
||||
playSound: notificationData.playSound !== false,
|
||||
soundName: notificationData.soundName || 'default',
|
||||
vibrate: notificationData.vibrate !== false,
|
||||
priority: notificationData.priority || 'high',
|
||||
visibility: notificationData.visibility || 'private',
|
||||
importance: 'high',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a custom notification should be shown based on app state
|
||||
*/
|
||||
export const shouldShowCustomNotification = (appState: AppStateStatus): boolean => {
|
||||
// Show notification when app is in background or inactive
|
||||
return appState !== 'active';
|
||||
};
|
||||
|
||||
/**
|
||||
* Test function to trigger a sample notification
|
||||
*/
|
||||
export const testNotification = (): void => {
|
||||
showLocalNotification({
|
||||
title: 'Test Notification',
|
||||
message: 'This is a test notification from T4B Chat app',
|
||||
channelId: 'default-channel-id',
|
||||
playSound: true,
|
||||
vibrate: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancels all notifications
|
||||
*/
|
||||
export const cancelAllNotifications = (): void => {
|
||||
PushNotification.cancelAllLocalNotifications();
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancels a specific notification by ID
|
||||
*/
|
||||
export const cancelNotification = (notificationId: string): void => {
|
||||
PushNotification.cancelLocalNotifications({ id: notificationId });
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the notification count
|
||||
*/
|
||||
export const getNotificationCount = (callback: (count: number) => void): void => {
|
||||
PushNotification.getApplicationIconBadgeNumber(callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the notification count
|
||||
*/
|
||||
export const setNotificationCount = (count: number): void => {
|
||||
PushNotification.setApplicationIconBadgeNumber(count);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears all notifications and badge count
|
||||
*/
|
||||
export const clearAllNotifications = (): void => {
|
||||
cancelAllNotifications();
|
||||
setNotificationCount(0);
|
||||
};
|
||||
279
utilities/permissionUtils.ts
Normal file
@ -0,0 +1,279 @@
|
||||
import { Platform, Alert } from 'react-native';
|
||||
import messaging, { AuthorizationStatus } from '@react-native-firebase/messaging';
|
||||
import { request, PERMISSIONS, RESULTS, Permission } from 'react-native-permissions';
|
||||
|
||||
export interface PermissionResult {
|
||||
granted: boolean;
|
||||
status: string;
|
||||
needsRequest: boolean;
|
||||
platform: string;
|
||||
androidVersion?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the device requires explicit notification permission request
|
||||
* Android 13+ (API 33+) requires POST_NOTIFICATIONS permission
|
||||
* Older Android versions don't need explicit permission for notifications
|
||||
*/
|
||||
export function needsNotificationPermission(): boolean {
|
||||
if (Platform.OS === 'ios') {
|
||||
return true; // iOS always requires permission
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
// Android 13+ (API level 33+) requires POST_NOTIFICATIONS permission
|
||||
return Platform.Version >= 33;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate notification permission constant based on platform
|
||||
*/
|
||||
export function getNotificationPermission(): Permission | null {
|
||||
if (Platform.OS === 'android') {
|
||||
//@ts-ignore
|
||||
return 'android.permission.POST_NOTIFICATIONS';
|
||||
}
|
||||
|
||||
// iOS uses Firebase messaging permission, not react-native-permissions
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request notification permission for Android 13+
|
||||
* Returns permission status information
|
||||
*/
|
||||
export async function requestNotificationPermission(): Promise<PermissionResult> {
|
||||
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
// iOS uses Firebase messaging permission
|
||||
try {
|
||||
|
||||
const authStatus = await messaging().requestPermission();
|
||||
const granted = authStatus === AuthorizationStatus.AUTHORIZED ||
|
||||
authStatus === AuthorizationStatus.PROVISIONAL;
|
||||
|
||||
|
||||
|
||||
return {
|
||||
granted,
|
||||
status: AuthorizationStatus[authStatus],
|
||||
needsRequest: true,
|
||||
platform: 'ios'
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
return {
|
||||
granted: false,
|
||||
status: 'ERROR',
|
||||
needsRequest: true,
|
||||
platform: 'ios'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
const androidVersion = Platform.Version as number;
|
||||
const permission = getNotificationPermission();
|
||||
|
||||
|
||||
|
||||
if (!permission) {
|
||||
// Older Android versions don't need explicit permission
|
||||
|
||||
return {
|
||||
granted: true,
|
||||
status: 'GRANTED',
|
||||
needsRequest: false,
|
||||
platform: 'android',
|
||||
androidVersion
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const result = await request(permission);
|
||||
|
||||
|
||||
|
||||
return {
|
||||
granted: result === RESULTS.GRANTED,
|
||||
status: result,
|
||||
needsRequest: true,
|
||||
platform: 'android',
|
||||
androidVersion
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
return {
|
||||
granted: false,
|
||||
status: 'ERROR',
|
||||
needsRequest: true,
|
||||
platform: 'android',
|
||||
androidVersion
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
granted: false,
|
||||
status: 'UNSUPPORTED',
|
||||
needsRequest: false,
|
||||
platform: 'unknown'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check current notification permission status
|
||||
*/
|
||||
export async function checkNotificationPermission(): Promise<PermissionResult> {
|
||||
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
try {
|
||||
|
||||
const authStatus = await messaging().hasPermission();
|
||||
const granted = authStatus === AuthorizationStatus.AUTHORIZED ||
|
||||
authStatus === AuthorizationStatus.PROVISIONAL;
|
||||
|
||||
|
||||
|
||||
return {
|
||||
granted,
|
||||
status: AuthorizationStatus[authStatus],
|
||||
needsRequest: !granted,
|
||||
platform: 'ios'
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
return {
|
||||
granted: false,
|
||||
status: 'ERROR',
|
||||
needsRequest: true,
|
||||
platform: 'ios'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
const androidVersion = Platform.Version as number;
|
||||
const permission = getNotificationPermission();
|
||||
|
||||
|
||||
if (!permission) {
|
||||
// Older Android versions - assume granted
|
||||
|
||||
return {
|
||||
granted: true,
|
||||
status: 'GRANTED',
|
||||
needsRequest: false,
|
||||
platform: 'android',
|
||||
androidVersion
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const { check } = require('react-native-permissions');
|
||||
const result = await check(permission);
|
||||
|
||||
|
||||
|
||||
|
||||
return {
|
||||
granted: result === RESULTS.GRANTED,
|
||||
status: result,
|
||||
needsRequest: result !== RESULTS.GRANTED,
|
||||
platform: 'android',
|
||||
androidVersion
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
return {
|
||||
granted: false,
|
||||
status: 'ERROR',
|
||||
needsRequest: true,
|
||||
platform: 'android',
|
||||
androidVersion
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
granted: false,
|
||||
status: 'UNSUPPORTED',
|
||||
needsRequest: false,
|
||||
platform: 'unknown'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show permission explanation dialog for Android
|
||||
*/
|
||||
export function showPermissionExplanation(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
Alert.alert(
|
||||
'Notification Permission',
|
||||
'This app needs notification permission to send you chat messages and updates. You can change this later in your device settings.',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
onPress: () => resolve(false),
|
||||
},
|
||||
{
|
||||
text: 'Allow',
|
||||
onPress: () => resolve(true),
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show settings redirect dialog when permission is denied
|
||||
*/
|
||||
export function showSettingsDialog(): void {
|
||||
Alert.alert(
|
||||
'Permission Required',
|
||||
'Notification permission is required for this app to work properly. Please enable it in your device settings.',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Open Settings',
|
||||
onPress: () => {
|
||||
// You can use react-native-permissions to open settings
|
||||
// Linking.openSettings();
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug function to test permission flow
|
||||
* Call this from your app to see detailed permission information
|
||||
*/
|
||||
export async function debugPermissionFlow(): Promise<void> {
|
||||
|
||||
|
||||
const permission = getNotificationPermission();
|
||||
|
||||
|
||||
const currentStatus = await checkNotificationPermission();
|
||||
|
||||
|
||||
if (!currentStatus.granted && currentStatus.needsRequest) {
|
||||
|
||||
const requestResult = await requestNotificationPermission();
|
||||
;
|
||||
}
|
||||
|
||||
console.log('🔧 === END DEBUG ===');
|
||||
}
|
||||
233
utilities/webViewUtils.ts
Normal file
@ -0,0 +1,233 @@
|
||||
/**
|
||||
* WebView Utilities
|
||||
* Handles WebView management, navigation, and message handling
|
||||
*/
|
||||
|
||||
import { WebViewNavigation } from 'react-native-webview';
|
||||
import { Linking, Platform } from 'react-native';
|
||||
import { ALLOWED_DOMAIN } from './constants';
|
||||
|
||||
/**
|
||||
* Interface for WebView message data
|
||||
*/
|
||||
export interface WebViewMessageData {
|
||||
type?: string;
|
||||
cookies?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for WebView navigation state
|
||||
*/
|
||||
export interface WebViewNavigationState {
|
||||
currentChannel: string | null;
|
||||
isNavigating: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebView Handler Class
|
||||
*/
|
||||
export class WebViewHandler {
|
||||
private webViewRef: React.RefObject<any>;
|
||||
private updateWebViewUrl: (url: string) => void;
|
||||
private navigationState: WebViewNavigationState;
|
||||
|
||||
constructor(
|
||||
webViewRef: React.RefObject<any>,
|
||||
updateWebViewUrl: (url: string) => void,
|
||||
navigationState: WebViewNavigationState
|
||||
) {
|
||||
this.webViewRef = webViewRef;
|
||||
this.updateWebViewUrl = updateWebViewUrl;
|
||||
this.navigationState = navigationState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle WebView navigation requests
|
||||
* @param request - Navigation request from WebView
|
||||
* @returns boolean indicating whether to allow navigation
|
||||
*/
|
||||
handleNavigation(request: WebViewNavigation): boolean {
|
||||
const { url } = request;
|
||||
|
||||
console.log('🧭 Navigation to:', url);
|
||||
|
||||
// Validate URL before processing
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
console.error("❌ Invalid URL detected:", url, error);
|
||||
return false; // Block invalid URLs
|
||||
}
|
||||
|
||||
// 🧪 Test notification when navigating to specific URL (temporary for testing)
|
||||
if (url.includes('test-notification')) {
|
||||
console.log('🧪 Test notification triggered via URL');
|
||||
// You can trigger test notification here if needed
|
||||
return false;
|
||||
}
|
||||
|
||||
// ✅ Stay inside Odoo instance
|
||||
if (
|
||||
url.startsWith(`${ALLOWED_DOMAIN}`) ||
|
||||
url.startsWith(`${ALLOWED_DOMAIN}`)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 🌍 Open external links in default browser
|
||||
Linking.openURL(url).catch((err) =>
|
||||
console.error("Couldn't open external link", err)
|
||||
);
|
||||
|
||||
return false; // block WebView from handling it
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle messages from WebView
|
||||
* @param event - Message event from WebView
|
||||
* @param setWebViewCookies - Function to set cookies
|
||||
*/
|
||||
handleWebViewMessage(
|
||||
event: any,
|
||||
setWebViewCookies: (cookies: string | null) => void
|
||||
): void {
|
||||
try {
|
||||
const messageData = event.nativeEvent.data;
|
||||
|
||||
// Try to parse as JSON first
|
||||
try {
|
||||
const data: WebViewMessageData = JSON.parse(messageData);
|
||||
console.log('📨 WebView JSON message received:', data);
|
||||
|
||||
// Handle cookie extraction (if needed)
|
||||
if (data.type === 'cookies') {
|
||||
console.log('🍪 Cookies from WebView:', data.cookies);
|
||||
setWebViewCookies(data.cookies || null);
|
||||
}
|
||||
} catch (jsonError) {
|
||||
// If JSON parsing fails, treat as plain text message
|
||||
console.log('📨 WebView text message received:', messageData);
|
||||
|
||||
// Handle navigation feedback messages
|
||||
if (messageData.startsWith('NAVIGATION_RESULT:')) {
|
||||
const result = messageData.replace('NAVIGATION_RESULT:', '');
|
||||
console.log('🔗 Navigation Result:', result);
|
||||
}
|
||||
// Handle other plain text messages here if needed
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error handling WebView message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle WebView load completion
|
||||
* @param currentChannel - Current channel being navigated to
|
||||
* @param isNavigating - Whether navigation is in progress
|
||||
*/
|
||||
handleWebViewLoadEnd(currentChannel: string | null, isNavigating: boolean): void {
|
||||
console.log('🔗 WebView loaded, current channel:', currentChannel, 'isNavigating:', isNavigating);
|
||||
|
||||
// Check if we successfully navigated to the target channel
|
||||
if (currentChannel && this.webViewRef.current) {
|
||||
console.log('🔗 WebView loaded - checking if navigation was successful');
|
||||
|
||||
// Add a simple debug script to check current URL
|
||||
const checkScript = `
|
||||
(function() {
|
||||
try {
|
||||
console.log('🔗 WebView loaded - Current URL:', window.location.href);
|
||||
console.log('🔗 WebView loaded - Current hash:', window.location.hash);
|
||||
|
||||
// Check if we're on the target channel page
|
||||
if (window.location.hash.includes('mail.channel_${currentChannel}')) {
|
||||
console.log('✅ Successfully navigated to channel ${currentChannel}');
|
||||
if (window.ReactNativeWebView) {
|
||||
window.ReactNativeWebView.postMessage('NAVIGATION_RESULT:Successfully navigated to channel ${currentChannel}');
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ Not on target channel page yet');
|
||||
console.log('🔗 Expected: mail.channel_${currentChannel}');
|
||||
console.log('🔗 Actual hash:', window.location.hash);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error in check script:', error);
|
||||
}
|
||||
})();
|
||||
true;
|
||||
`;
|
||||
|
||||
this.webViewRef.current.injectJavaScript(checkScript);
|
||||
}
|
||||
|
||||
// If we're navigating and have a current channel, we might need to retry
|
||||
if (currentChannel && isNavigating) {
|
||||
console.log('🔗 Navigation in progress - checking if retry needed');
|
||||
// Don't retry immediately, let the two-step navigation complete
|
||||
} else if (currentChannel && !isNavigating) {
|
||||
console.log('🔗 Navigation complete - no retry needed');
|
||||
} else {
|
||||
console.log('🔗 No active navigation');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject debug script into WebView
|
||||
*/
|
||||
injectDebugScript(): void {
|
||||
if (this.webViewRef.current) {
|
||||
const debugScript = `
|
||||
(function() {
|
||||
console.log('🔍 WebView Debug Info:');
|
||||
console.log(' - Current URL:', window.location.href);
|
||||
console.log(' - Current hash:', window.location.hash);
|
||||
console.log(' - Document ready state:', document.readyState);
|
||||
console.log(' - Available chat elements:', document.querySelectorAll('[class*="mail"], [class*="chat"], [data-model*="mail"]').length);
|
||||
console.log(' - Available channel elements:', document.querySelectorAll('[data-channel-id], [data-id*="mail.channel"], [data-oe-id*="mail.channel"]').length);
|
||||
|
||||
// List all elements with channel-related attributes
|
||||
const channelElements = document.querySelectorAll('[data-channel-id], [data-id*="mail.channel"], [data-oe-id*="mail.channel"], a[href*="mail.channel"]');
|
||||
console.log('🔍 Channel elements found:', channelElements.length);
|
||||
channelElements.forEach((el, index) => {
|
||||
console.log(\` \${index + 1}. \${el.tagName} - \${el.className} - \${el.getAttribute('data-channel-id') || el.getAttribute('data-id') || el.getAttribute('href')}\`);
|
||||
});
|
||||
|
||||
// Check Odoo availability
|
||||
console.log('🔍 Odoo Debug:');
|
||||
console.log(' - window.odoo exists:', !!window.odoo);
|
||||
console.log(' - odoo.loader exists:', !!(window.odoo && window.odoo.loader));
|
||||
console.log(' - odoo.services exists:', !!(window.odoo && window.odoo.services));
|
||||
console.log(' - odoo.__DEBUG__ exists:', !!(window.odoo && window.odoo.__DEBUG__));
|
||||
|
||||
if (window.odoo && window.odoo.__DEBUG__) {
|
||||
const services = Object.keys(window.odoo.__DEBUG__.services || {});
|
||||
console.log(' - Available services:', services);
|
||||
}
|
||||
})();
|
||||
true;
|
||||
`;
|
||||
this.webViewRef.current.injectJavaScript(debugScript);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WebView console messages
|
||||
* @param event - Console message event
|
||||
*/
|
||||
handleConsoleMessage(event: any): void {
|
||||
console.log('🌐 WebView Console:', event.nativeEvent.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create WebView handler instance
|
||||
*/
|
||||
export function createWebViewHandler(
|
||||
webViewRef: React.RefObject<any>,
|
||||
updateWebViewUrl: (url: string) => void,
|
||||
navigationState: WebViewNavigationState
|
||||
): WebViewHandler {
|
||||
return new WebViewHandler(webViewRef, updateWebViewUrl, navigationState);
|
||||
}
|
||||