now deep linking completed and loader also added for the app
362
App.tsx
@ -1,131 +1,269 @@
|
|||||||
/**
|
import React, { JSX, useEffect, useState } from 'react';
|
||||||
* Sample React Native App
|
import { SafeAreaView, StyleSheet, Platform, StatusBar, AppState, View, Text, ActivityIndicator, Animated } from 'react-native';
|
||||||
* https://github.com/facebook/react-native
|
import { WebView } from 'react-native-webview';
|
||||||
*
|
|
||||||
* @format
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
// Utility imports
|
||||||
import type {PropsWithChildren} from 'react';
|
|
||||||
import {
|
import {
|
||||||
ScrollView,
|
createNotificationChannel,
|
||||||
StatusBar,
|
configureNotificationSettings,
|
||||||
StyleSheet,
|
testNotification
|
||||||
Text,
|
} from './utilities/notificationUtils';
|
||||||
useColorScheme,
|
|
||||||
View,
|
|
||||||
} from 'react-native';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Colors,
|
createDeepLinkHandler,
|
||||||
DebugInstructions,
|
NotificationData
|
||||||
Header,
|
} from './utilities/deepLinkUtils';
|
||||||
LearnMoreLinks,
|
import {
|
||||||
ReloadInstructions,
|
createWebViewHandler
|
||||||
} from 'react-native/Libraries/NewAppScreen';
|
} 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<{
|
// Suppress Firebase deprecation warnings (temporary until Firebase v22+ is stable)
|
||||||
title: string;
|
const originalWarn = console.warn;
|
||||||
}>;
|
console.warn = (...args) => {
|
||||||
|
if (args[0] && typeof args[0] === 'string' &&
|
||||||
function Section({children, title}: SectionProps): React.JSX.Element {
|
args[0].includes('This method is deprecated') &&
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
args[0].includes('Firebase')) {
|
||||||
return (
|
return; // Suppress only Firebase deprecation warnings
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
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 = {
|
// Track app state to avoid duplicate notifications
|
||||||
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
|
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
// Create utility handlers
|
||||||
* To keep the template simple and small we're adding padding to prevent view
|
const cookieHandler = createCookieHandler(setWebViewCookies);
|
||||||
* from rendering under the System UI.
|
const webViewHandler = createWebViewHandler(webViewRef, updateWebViewUrl, {
|
||||||
* For bigger apps the recommendation is to use `react-native-safe-area-context`:
|
currentChannel,
|
||||||
* https://github.com/AppAndFlow/react-native-safe-area-context
|
isNavigating
|
||||||
*
|
});
|
||||||
* You can read more about it here:
|
|
||||||
* https://github.com/react-native-community/discussions-and-proposals/discussions/827
|
// Navigation state for deep link handler
|
||||||
*/
|
const navigationState = {
|
||||||
const safePadding = '5%';
|
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 (
|
return (
|
||||||
<View style={backgroundStyle}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar
|
<WebView
|
||||||
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
|
ref={webViewRef}
|
||||||
backgroundColor={backgroundStyle.backgroundColor}
|
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}>
|
{/* Dynamic Beautiful Custom Loader */}
|
||||||
<View style={{paddingRight: safePadding}}>
|
{loaderType === 'odoo' && createOdooLoader(isLoading, loadingText)}
|
||||||
<Header/>
|
{loaderType === 'navigation' && createNavigationLoader(isLoading, loadingText)}
|
||||||
</View>
|
{loaderType === 'chat' && createChatLoader(isLoading, loadingText)}
|
||||||
<View
|
</SafeAreaView>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
sectionContainer: {
|
container: {
|
||||||
marginTop: 32,
|
flex: 1,
|
||||||
paddingHorizontal: 24,
|
// paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight ?? 0 : 0,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
webview: {
|
||||||
fontSize: 24,
|
flex: 1,
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
sectionDescription: {
|
|
||||||
marginTop: 8,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: '400',
|
|
||||||
},
|
|
||||||
highlight: {
|
|
||||||
fontWeight: '700',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
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: "com.android.application"
|
||||||
apply plugin: "org.jetbrains.kotlin.android"
|
apply plugin: "org.jetbrains.kotlin.android"
|
||||||
apply plugin: "com.facebook.react"
|
apply plugin: "com.facebook.react"
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the configuration block to customize your React Native Android app.
|
* 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.
|
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||||
*/
|
*/
|
||||||
def enableProguardInReleaseBuilds = false
|
def enableProguardInReleaseBuilds = false
|
||||||
|
def enableSeparateBuildPerCPUArchitecture = true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The preferred build flavor of JavaScriptCore (JSC)
|
* The preferred build flavor of JavaScriptCore (JSC)
|
||||||
@ -78,6 +80,19 @@ android {
|
|||||||
compileSdk rootProject.ext.compileSdkVersion
|
compileSdk rootProject.ext.compileSdkVersion
|
||||||
|
|
||||||
namespace "com.t4b_chat"
|
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 {
|
defaultConfig {
|
||||||
applicationId "com.t4b_chat"
|
applicationId "com.t4b_chat"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
@ -111,9 +126,15 @@ dependencies {
|
|||||||
// The version of react-native is set by the React Native Gradle Plugin
|
// The version of react-native is set by the React Native Gradle Plugin
|
||||||
implementation("com.facebook.react:react-android")
|
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()) {
|
if (hermesEnabled.toBoolean()) {
|
||||||
implementation("com.facebook.react:hermes-android")
|
implementation("com.facebook.react:hermes-android")
|
||||||
} else {
|
} else {
|
||||||
implementation jscFlavor
|
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">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<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
|
<application
|
||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
@ -9,6 +12,8 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
networkSecurityConfig="@xml/network_security_config"
|
||||||
android:supportsRtl="true">
|
android:supportsRtl="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
@ -22,5 +27,14 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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>
|
</application>
|
||||||
</manifest>
|
</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">
|
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||||
|
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</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.android.tools.build:gradle")
|
||||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-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
|
# Android operating system, and which are packaged with your app's APK
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
android.useAndroidX=true
|
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.
|
# Use this property to specify which architecture you want to build.
|
||||||
# You can also override it from the CLI using
|
# 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
|
# your application. You should enable this flag either if you want
|
||||||
# to write custom TurboModules/Fabric components OR use libraries that
|
# to write custom TurboModules/Fabric components OR use libraries that
|
||||||
# are providing them.
|
# are providing them.
|
||||||
newArchEnabled=true
|
newArchEnabled=false
|
||||||
|
|
||||||
# Use this property to enable or disable the Hermes JS engine.
|
# Use this property to enable or disable the Hermes JS engine.
|
||||||
# If set to false, you will be using JSC instead.
|
# If set to false, you will be using JSC instead.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
2285
package-lock.json
generated
30
package.json
@ -10,8 +10,36 @@
|
|||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": "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": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@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);
|
||||||
|
}
|
||||||