diff --git a/android/app/build.gradle b/android/app/build.gradle index e5c4585..50aaf4c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -93,7 +93,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 - versionName "1.4.3" + versionName "1.5.3" } signingConfigs { debug { @@ -154,7 +154,11 @@ dependencies { def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true"; def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true"; def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true"; + implementation project(':react-native-tcp-socket') + implementation project(':react-native-keyevent') + implementation project(':react-native-bluetooth-classic') + if (isGifEnabled) { // For animated gif support implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}") diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 3ec2507..f3b51e8 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,7 +1,16 @@ + + + + + + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 403ab21..45f79d2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,7 +6,10 @@ - + + + + diff --git a/android/app/src/main/java/com/anonymous/Assemblr/MainActivity.kt b/android/app/src/main/java/com/anonymous/Assemblr/MainActivity.kt index fefb5d8..d6aa57a 100644 --- a/android/app/src/main/java/com/anonymous/Assemblr/MainActivity.kt +++ b/android/app/src/main/java/com/anonymous/Assemblr/MainActivity.kt @@ -1,65 +1,88 @@ package com.anonymous.Assemblr + import expo.modules.splashscreen.SplashScreenManager import android.os.Build import android.os.Bundle +import android.view.KeyEvent import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultReactActivityDelegate +import com.github.kevinejohn.keyevent.KeyEventModule import expo.modules.ReactActivityDelegateWrapper class MainActivity : ReactActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - // Set the theme to AppTheme BEFORE onCreate to support - // coloring the background, status bar, and navigation bar. - // This is required for expo-splash-screen. - // setTheme(R.style.AppTheme); - // @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af - SplashScreenManager.registerOnActivity(this) - // @generated end expo-splashscreen - super.onCreate(null) - } + override fun onCreate(savedInstanceState: Bundle?) { + // Set the theme to AppTheme BEFORE onCreate to support + // coloring the background, status bar, and navigation bar. + // This is required for expo-splash-screen. + // setTheme(R.style.AppTheme); + // @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af + SplashScreenManager.registerOnActivity(this) + // @generated end expo-splashscreen + super.onCreate(null) + } - /** - * Returns the name of the main component registered from JavaScript. This is used to schedule - * rendering of the component. - */ - override fun getMainComponentName(): String = "main" + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String = "main" - /** - * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] - * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] - */ - override fun createReactActivityDelegate(): ReactActivityDelegate { - return ReactActivityDelegateWrapper( - this, - BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, - object : DefaultReactActivityDelegate( - this, - mainComponentName, - fabricEnabled - ){}) - } + /** + * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] + * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] + */ + override fun createReactActivityDelegate(): ReactActivityDelegate { + return ReactActivityDelegateWrapper( + this, + BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, + object : DefaultReactActivityDelegate( + this, + mainComponentName, + fabricEnabled + ) {}) + } - /** - * Align the back button behavior with Android S - * where moving root activities to background instead of finishing activities. - * @see onBackPressed - */ - override fun invokeDefaultOnBackPressed() { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { - if (!moveTaskToBack(false)) { - // For non-root activities, use the default implementation to finish them. - super.invokeDefaultOnBackPressed() - } - return - } + /** + * Align the back button behavior with Android S + * where moving root activities to background instead of finishing activities. + * @see onBackPressed + */ + override fun invokeDefaultOnBackPressed() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (!moveTaskToBack(false)) { + // For non-root activities, use the default implementation to finish them. + super.invokeDefaultOnBackPressed() + } + return + } - // Use the default back button implementation on Android S - // because it's doing more than [Activity.moveTaskToBack] in fact. - super.invokeDefaultOnBackPressed() - } + // Use the default back button implementation on Android S + // because it's doing more than [Activity.moveTaskToBack] in fact. + super.invokeDefaultOnBackPressed() + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + KeyEventModule.getInstance().onKeyDownEvent(keyCode, event) +// super.onKeyDown(keyCode, event) + return true + } + + override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { + KeyEventModule.getInstance().onKeyUpEvent(keyCode, event); +// super.onKeyUp(keyCode, event) + return true; + } + + + + override fun onKeyMultiple(keyCode: Int, repeatCount: Int, event: KeyEvent): Boolean { + KeyEventModule.getInstance().onKeyMultipleEvent(keyCode, repeatCount, event) + return true; +// return super.onKeyMultiple(keyCode, repeatCount, event) + } } diff --git a/android/app/src/main/java/com/anonymous/Assemblr/MainApplication.kt b/android/app/src/main/java/com/anonymous/Assemblr/MainApplication.kt index 53604c8..9883a26 100644 --- a/android/app/src/main/java/com/anonymous/Assemblr/MainApplication.kt +++ b/android/app/src/main/java/com/anonymous/Assemblr/MainApplication.kt @@ -12,10 +12,12 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.soloader.SoLoader - +import com.github.kevinejohn.keyevent.KeyEventPackage import expo.modules.ApplicationLifecycleDispatcher import expo.modules.ReactNativeHostWrapper import com.asterinet.react.tcpsocket.TcpSocketPackage; +import kjd.reactnative.bluetooth.RNBluetoothClassicPackage; + class MainApplication : Application(), ReactApplication { override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper( @@ -26,6 +28,8 @@ class MainApplication : Application(), ReactApplication { packages.add(PdfToBitmapPackage()) packages.add(TcpSocketPackage()) packages.add(MyTcpSocketPackage()) + packages.add(KeyEventPackage()) + packages.add(RNBluetoothClassicPackage()) return packages } @@ -55,4 +59,5 @@ class MainApplication : Application(), ReactApplication { super.onConfigurationChanged(newConfig) ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) } + } diff --git a/android/settings.gradle b/android/settings.gradle index 1c15b7d..ce268e8 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -37,5 +37,12 @@ expoAutolinking.useExpoVersionCatalog() include ':app' includeBuild(expoAutolinking.reactNativeGradlePlugin) + include ':react-native-tcp-socket' project(':react-native-tcp-socket').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-tcp-socket/android') + +include ':react-native-keyevent' +project(':react-native-keyevent').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keyevent/android') + +include ':react-native-bluetooth-classic' +project(':react-native-bluetooth-classic').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-bluetooth-classic/android') \ No newline at end of file diff --git a/app.json b/app.json index 2cf7eb1..748147a 100644 --- a/app.json +++ b/app.json @@ -12,7 +12,7 @@ ], "name": "Assemblr", "slug": "Assemblr", - "version": "1.4.3", + "version": "1.5.3", "orientation": "portrait", "icon": "./src/assets/icon.png", "userInterfaceStyle": "light", diff --git a/package.json b/package.json index 5f2a7ec..25d0e25 100644 --- a/package.json +++ b/package.json @@ -46,9 +46,12 @@ "react-hook-form": "^7.57.0", "react-native": "0.79.2", "react-native-animatable": "^1.4.0", + "react-native-bluetooth-classic": "^1.73.0-rc.13", + "react-native-bluetooth-serial": "lucassouza16/react-native-bluetooth-serial", "react-native-gesture-handler": "~2.24.0", "react-native-image-pan-zoom": "^2.1.12", "react-native-image-zoom-viewer": "^3.0.1", + "react-native-keyevent": "^0.3.2", "react-native-modal": "^13.0.1", "react-native-paper": "^5.12.5", "react-native-picker-select": "^9.3.1", @@ -60,6 +63,7 @@ "react-native-responsive-fontsize": "^0.5.1", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.10.0", + "react-native-serialport-bluetooth": "^0.4.11", "react-native-svg": "15.11.2", "react-native-tcp-socket": "^6.3.0", "react-native-toast-message": "^2.2.1", diff --git a/src/App.tsx b/src/App.tsx index 35666fc..65cf771 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,18 @@ import {StyleSheet} from 'react-native'; import {Provider} from "react-redux"; import {store} from "./redux/store"; -import React from "react"; +import React, {useEffect} from "react"; import CommonPage from "./screens/CommonPage/CommonPage"; import {BottomSheetModalProvider} from "@gorhom/bottom-sheet"; import {GestureHandlerRootView} from "react-native-gesture-handler"; +import KeyEvent from "react-native-keyevent"; export default function App() { return ( + <> + diff --git a/src/assets/icons/scanner.png b/src/assets/icons/scanner.png new file mode 100644 index 0000000..58a8828 Binary files /dev/null and b/src/assets/icons/scanner.png differ diff --git a/src/components/SearchBar/ScanModal.tsx b/src/components/SearchBar/ScanModal.tsx index b8be305..fbf3680 100644 --- a/src/components/SearchBar/ScanModal.tsx +++ b/src/components/SearchBar/ScanModal.tsx @@ -1,5 +1,5 @@ import {FC, useEffect, useRef} from "react"; -import {GestureResponderEvent, StyleSheet, Text, TextInput, View} from "react-native"; +import {StyleSheet, TextInput, View} from "react-native"; import {responsiveHeight, responsiveWidth} from "react-native-responsive-dimensions"; import {RFPercentage} from "react-native-responsive-fontsize"; import Modal from "react-native-modal"; @@ -11,12 +11,14 @@ import {closeScanModal, setScannedData} from "../../features/scanModal/scanModal const ScanModal: FC = () => { const inputRef = useRef(null); + const scannerState = useSelector((state: RootState) => state.scanner); const state = useSelector((state: RootState) => state.scanModal); const getDefaultLabel = () => "Наведите сканер на штрихкод товара или заказа"; const dispatch = useDispatch(); useEffect(() => { if (state.isVisible) inputRef.current?.focus(); }, [state.isVisible]); + return ( diff --git a/src/css/colors.tsx b/src/css/colors.tsx index dcd1e47..ad80b20 100644 --- a/src/css/colors.tsx +++ b/src/css/colors.tsx @@ -1,3 +1,5 @@ export const background = "#F5F5F5"; export const blue = "#2478F8"; -export const gray = '#A5A5A5'; \ No newline at end of file +export const gray = '#A5A5A5'; +export const red = "#e03131" +export const blueButton = '#2478F8' \ No newline at end of file diff --git a/src/features/scannerState/scannerState.ts b/src/features/scannerState/scannerState.ts new file mode 100644 index 0000000..6c27061 --- /dev/null +++ b/src/features/scannerState/scannerState.ts @@ -0,0 +1,30 @@ +import {BluetoothDevice} from "react-native-bluetooth-classic"; +import {createSlice, PayloadAction} from "@reduxjs/toolkit"; + +export interface ScannerState { + isConnected: boolean; + deviceAddress?: string; +} + +const initialState: ScannerState = { + isConnected: false, + deviceAddress: undefined +} + +export const scannerSlice = createSlice({ + name: "scannerState", + initialState, + reducers: { + setDevice(state, action: PayloadAction<{ deviceAddress: string }>) { + state.deviceAddress = action.payload.deviceAddress; + }, + setIsConnected(state, action: PayloadAction<{ isConnected: boolean }>) { + state.isConnected = action.payload.isConnected + } + + + } +}) + +export const {setDevice, setIsConnected} = scannerSlice.actions +export default scannerSlice.reducer \ No newline at end of file diff --git a/src/redux/store.ts b/src/redux/store.ts index 5acf8c5..be25fe3 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -15,6 +15,7 @@ import cancelAssemblyModal from 'features/cancelAssemblyModal/cancelAssemblyModa import balanceReducer from 'features/balance/balanceSlice'; import animationsReducer from 'features/animations/animationsSlice'; import filterReducer from 'features/filterSlice/filterSlice'; +import scannerReducer from 'features/scannerState/scannerState'; import {useDispatch} from "react-redux"; export const store = configureStore({ @@ -33,7 +34,8 @@ export const store = configureStore({ cancelAssemblyModal: cancelAssemblyModal, balance: balanceReducer, animations: animationsReducer, - filter:filterReducer + filter: filterReducer, + scanner: scannerReducer }, }); diff --git a/src/screens/CommonPage/CommonPage.tsx b/src/screens/CommonPage/CommonPage.tsx index f57e09c..5a5731e 100644 --- a/src/screens/CommonPage/CommonPage.tsx +++ b/src/screens/CommonPage/CommonPage.tsx @@ -66,6 +66,7 @@ function CommonPage() { }) } const checkUpdates = async () => { + return const currentVersion = Application.nativeApplicationVersion; applicationApi.getVersion('assemblr').then(({latest_version}) => { diff --git a/src/screens/LoginScreen/LoginScreen.tsx b/src/screens/LoginScreen/LoginScreen.tsx index 771a4f7..bbf867b 100644 --- a/src/screens/LoginScreen/LoginScreen.tsx +++ b/src/screens/LoginScreen/LoginScreen.tsx @@ -11,7 +11,6 @@ import {useDispatch} from "react-redux"; import {login} from "../../features/auth/authSlice"; import Toast from "react-native-toast-message"; import * as SecureStore from 'expo-secure-store'; -import KeyEvent from "react-native-keyevent"; function LoginScreen() { const dispatch = useAppDispatch(); diff --git a/src/screens/MainScreen/MainScreen.tsx b/src/screens/MainScreen/MainScreen.tsx index aaf3e96..bb8794a 100644 --- a/src/screens/MainScreen/MainScreen.tsx +++ b/src/screens/MainScreen/MainScreen.tsx @@ -70,7 +70,7 @@ function MainScreen() { { name: "Money", component: moneyScreen, - icon: require('assets/icons/money.png'), + icon: require('assets/icons/scanner.png'), hidden: false }, { diff --git a/src/screens/MoneyScreen/MoneyScreen.tsx b/src/screens/MoneyScreen/MoneyScreen.tsx index e8ca5c1..6da1255 100644 --- a/src/screens/MoneyScreen/MoneyScreen.tsx +++ b/src/screens/MoneyScreen/MoneyScreen.tsx @@ -1,65 +1,157 @@ -import {Text, View} from "react-native"; +import {PermissionsAndroid, Platform, Text, TouchableOpacity, View} from "react-native"; +import {useEffect, useState} from "react"; +import {Permission} from "react-native/Libraries/PermissionsAndroid/PermissionsAndroid"; +import RNBluetoothClassic, {BluetoothDevice} from "react-native-bluetooth-classic"; +import {RFPercentage} from "react-native-responsive-fontsize"; +import {responsiveWidth} from "react-native-responsive-dimensions"; +import {blue, blueButton, red} from "../../css/colors"; import BasicButton from "../../components/BasicButton/BasicButton"; -import {useScanningContext} from "../../providers/ScanProvider"; -import printingApi from "../../api/printingApi"; -import {Zpl} from "react-native-zpl-code"; -import PrintingService from "../../utils/PrintingService"; +import {useSelector} from "react-redux"; +import {RootState, useAppDispatch} from "../../redux/store"; +import {setDevice, setIsConnected} from "../../features/scannerState/scannerState"; +import {closeScanModal, setScannedData} from "../../features/scanModal/scanModalSlice"; -async function generateZplCodes(images: string[]) { - const zplCodes = []; - for (const image of images) { - const zplBuilder = new Zpl.Builder(); - zplBuilder.setup({ - size: { - heightDots: 320, // 40mm at 203 DPI - widthDots: 464, // 58mm at 203 DPI - }, - labelHome: { - x: 0, - y: 0, - }, - labelTop: 0, - labeShift: 0, - orientation: "NORMAL", - media: { - type: "MARK_SENSING", - dots: 0, - }, - }); - - zplBuilder.image({ - uri: `data:image/png;base64,${image}`, - x: 0, - y: 0, - width: 464, - height: 320, - dither: true, - }); - - const zplCode = await zplBuilder.build(); - zplCodes.push(zplCode); - } - - return zplCodes; +type BluetoothSettingsState = { + enabled: boolean; + devices: BluetoothDevice[] +} +const DEFAULT_BLUETOOTH_SETTINGS_STATE: BluetoothSettingsState = { + enabled: false, + devices: [] } function MoneyScreen() { - const {scan} = useScanningContext(); - const a = "123"; - const onPress = () => { - printingApi.getLabel(391845).then(async result => { - PrintingService.getInstance().printPdf("", result).then(() => { - } - ).catch(error => { - console.error("Error printing:", error); - }); - }); + const [state, setState] = useState(DEFAULT_BLUETOOTH_SETTINGS_STATE); + const scannerState = useSelector((state: RootState) => state.scanner); + const dispatch = useAppDispatch(); + + async function requestBluetoothPermissions() { + if (Platform.OS === 'android') { + const permissions: Permission[] = []; + + if (Platform.Version >= 31) { // Android 12+ + permissions.push( + PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, + PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, + PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE, + PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION + ); + console.log("android12") + } else { // Below Android 12 + permissions.push( + PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION + ); + } + + const granted = await PermissionsAndroid.requestMultiple(permissions); + + + const hasBluetoothPermission = + (Platform.Version >= 31 && + granted['android.permission.BLUETOOTH_SCAN'] === PermissionsAndroid.RESULTS.GRANTED && + granted['android.permission.BLUETOOTH_CONNECT'] === PermissionsAndroid.RESULTS.GRANTED) || + (Platform.Version < 31 && + granted['android.permission.ACCESS_FINE_LOCATION'] === PermissionsAndroid.RESULTS.GRANTED); + + if (hasBluetoothPermission) { + console.log('Bluetooth permissions granted'); + } else { + console.warn('Bluetooth permissions denied'); + } + } } + + const initialize = async () => { + const isEnabled = await RNBluetoothClassic.isBluetoothEnabled(); + const devices = isEnabled ? await RNBluetoothClassic.getBondedDevices() : [] + setState(prevState => { + return { + ...prevState, + enabled: isEnabled, + devices: devices + } + }) + } + const setSelectedDevice = (device: BluetoothDevice) => { + dispatch(setDevice({deviceAddress: device.address})); + } + const onConnectPress = async () => { + + if (!scannerState.deviceAddress) return; + if (scannerState.isConnected) { + console.log("connected") + await RNBluetoothClassic.disconnectFromDevice(scannerState.deviceAddress); + return + } + console.log("connecting") + const connected = await RNBluetoothClassic.connectToDevice(scannerState.deviceAddress, { + secureSocket: false, + charset: "UTF-8", + delimiter: "\r\r" + }); + const isConnected = await connected.isConnected(); + dispatch(setIsConnected({isConnected})); + if (isConnected) { + RNBluetoothClassic.onDeviceRead(connected.address, event => { + dispatch(setScannedData(event.data)); + dispatch(closeScanModal()) + + }) + } + } + useEffect(() => { + const interval = setInterval(initialize, 1000); + + const {remove: removeOnConnectedListener} = RNBluetoothClassic.onDeviceConnected(event => { + console.log("Connected event") + dispatch(setIsConnected({isConnected: true})); + + }) + const {remove: removeOnDisconnectedListener} = RNBluetoothClassic.onDeviceDisconnected(event => { + dispatch(setIsConnected({isConnected: false})); + + }) + return () => { + clearInterval(interval); + removeOnConnectedListener(); + removeOnDisconnectedListener() + } + + }, []); return ( - - Money - onPress()} label={"test"}/> + + + Блютуз включен: {state.enabled ? "да" : "нет"} + + + + {state.devices.map(device => ( + setSelectedDevice(device)} key={device.address}> + + + {device.name} + + + + ))} + + + ) }