I need to get the detailed bluetooth data of the surrounding devices when I press the button. I use the react-native-ble-plx library for this, but this library does not integrate with android 12. I made some necessary permisson adjustments for this. On manifest.xml file and app.js files. When I run the code, I get the following error about the "scanDeviceStart" method.
TypeError: Cannot read property 'startDeviceScan' of null
What would you suggest me to solve this error? my codes are below
React-Native-ble-plx = https://dotintent.github.io/react-native-ble-plx/#blemanager
import React, { useState, useEffect } from 'react';
import {
SafeAreaView,
View,
Text,
Button,
StyleSheet,
PermissionsAndroid ,
FlatList,
} from 'react-native';
import BleManager from 'react-native-ble-plx';
import { PERMISSIONS, requestMultiple } from 'react-native-permissions';
import DeviceInfo from 'react-native-device-info';
const requestPermissions = async () => {
if (Platform.OS === 'android') {
const apiLevel = await DeviceInfo.getApiLevel();
if (apiLevel < 31) {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Location Permission',
message: 'Bluetooth Low Energy requires Location',
buttonNeutral: 'Ask Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
},
);
} else {
const result = await requestMultiple([
PERMISSIONS.ANDROID.BLUETOOTH_SCAN,
PERMISSIONS.ANDROID.BLUETOOTH_CONNECT,
PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
]);
const isGranted =
result['android.permission.BLUETOOTH_CONNECT'] ===
PermissionsAndroid.RESULTS.GRANTED &&
result['android.permission.BLUETOOTH_SCAN'] ===
PermissionsAndroid.RESULTS.GRANTED &&
result['android.permission.ACCESS_FINE_LOCATION'] ===
PermissionsAndroid.RESULTS.GRANTED;
}
}
};
const App = () => {
const [devices, setDevices] = useState([]);
const [scanning, setScanning] = useState(false);
const [bleManager, setBleManager] = useState(null);
function requestBluetoothPermissions(cb) {
if (Platform.OS === 'android') {
DeviceInfo.getApiLevel().then((apiLevel) => {
if (apiLevel < 31) {
PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Location Permission',
message: 'Bluetooth Low Energy requires Location',
buttonNeutral: 'Ask Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
},
).then((granted) => {
cb(granted === PermissionsAndroid.RESULTS.GRANTED);
});
} else {
requestMultiple([
PERMISSIONS.ANDROID.BLUETOOTH_SCAN,
PERMISSIONS.ANDROID.BLUETOOTH_CONNECT,
PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
]).then((result) => {
const isGranted =
result['android.permission.BLUETOOTH_CONNECT'] === PermissionsAndroid.RESULTS.GRANTED &&
result['android.permission.BLUETOOTH_SCAN'] === PermissionsAndroid.RESULTS.GRANTED &&
result['android.permission.ACCESS_FINE_LOCATION'] === PermissionsAndroid.RESULTS.GRANTED;
cb(isGranted);
});
}
});
} else {
cb(true);
}
}
useEffect(() => {
if (!bleManager) {
requestBluetoothPermissions((isGranted) => {
if (isGranted) {
const manager = new BleManager();
setBleManager(manager);
manager.startDeviceScan( [serialUUIDs.serviceUUID],
{scanMode: ScanMode.LowLatency}, (error, device) => {
if (error) {
console.log(error.message);
return;
}
console.log(device.name);
console.log(device.id);
if (device.name) {
setDevices((prevDevices) => [...prevDevices, device]);
}
});
setScanning(true);
}
});
}
}, []);
return (
<SafeAreaView>
<View>
{scanning ? (
<Button
title="Stop Scanning"
onPress={() => {
bleManager.stopDeviceScan();
setScanning(false);
}}
/>
) : (
<Button
title="Start Scanning"
onPress={() => {
requestBluetoothPermissions((isGranted) => {
if (isGranted) {
setDevices([]);
bleManager.startDeviceScan([serialUUIDs.serviceUUID],
{scanMode: ScanMode.LowLatency},(error, device) => {
if (error) {
console.log(error.message);
return;
}
console.log(device.name);
console.log(device.id);
if (device.name) {
setDevices((prevDevices) => [...prevDevices, device]);
}
});
setScanning(true);
}
});
}}
/>
)}
</View>
<FlatList
data={devices}
renderItem={({ item }) => (
<Text>
{item.name} ({item.id})
</Text>
)}
keyExtractor={(item) => item.id}
/>
</SafeAreaView>
);
};
export default App;
My app wasn't able to rerender on installation startup 'singleton' that I have created ONLY when I generate the apk file through eam build.
It is working when I use expo-go I have click on one of the other button (this case, my own create exercise button) for it to force rerender again. this create button sets a useState to render a 'Modal'
I am really tired of working on this. Hope there is some suggestions for this bug.
App.tsx
import {
Alert, LogBox
} from 'react-native'
import { ExercisesScreen } from './screens/ExercisesScreen'
import { SettingsScreen } from './screens/SettingsScreen'
import { PlanScreen } from './screens/ScheduleScreen'
import React, { useState, useEffect } from 'react'
import { NavigationContainer } from '#react-navigation/native'
import { createBottomTabNavigator } from '#react-navigation/bottom-tabs'
import {
init, resetTables, createScheduledItem, deleteScheduledItem, updateScheduledItem,
createExerciseMajorMuscleRelationship, createExercise,
deleteExerciseMajorMuscleRelationship, deleteExercise, updateExercise,
retrieveExerciseMajorMuscleRelationships, retrieveMajorMuscles,
retrieveScheduledItems, retrieveExercises, deleteFromExerciseAndScheduledItem
} from './dbhandler'
import {
ButtonSetProps, ContextProps, DialogState, Exercise, ExerciseState,
MajorMuscle, PushPullEnum, ScheduledItemState
} from './types'
import { ScheduledItem } from './types'
import Toast from 'react-native-simple-toast'
import Colors from './constants/Colors'
import { DateData } from 'react-native-calendars'
import { Ionicons, MaterialCommunityIcons, } from '#expo/vector-icons'
import _default from 'babel-plugin-transform-typescript-metadata'
import { ScheduleDialog } from './screens/ScheduleDialog'
import { ExerciseDialog } from './screens/ExerciseDialog'
import { styles } from './constants/styles'
import { SelectDateDialog } from './screens/SelectDateDialog'
import { StorageAccessFramework } from 'expo-file-system'
import {
ExerciseInformationText, EditExerciseText,
CreateExerciseText, ScheduledItemInformation,
EditScheduledItemText, DuplicateScheduledItemText, CreateScheduledItemText
} from './constants/strings'
import {
initialExerciseState, initialScheduledItemState, initialMajorMuscles,
initialEmm, initialDialogState, initialScheduledItem, ExerciseScreenContext,
ScheduledItemContext, SettingsScreenContext
} from './constants/initialValues'
import * as DocumentPicker from 'expo-document-picker'
LogBox.ignoreLogs(['Require cycle:'])
const Tab = createBottomTabNavigator()
export default function App() {
const [exerciseState, setExerciseState] = useState<ExerciseState>(initialExerciseState)
const [scheduledItemState, setScheduledItemState] =
useState<ScheduledItemState>(initialScheduledItemState)
const [dialogState, SetDialogState] = useState<DialogState>(initialDialogState)
const [emm, setEmm] = useState(initialEmm)
const [dropDownMajorMuscleNameSelected, setDropDownMajorMuscleValues] = useState([""])
const [dropDownExNameSelected, setDropDownExNameSelected] = useState("")
const [dropDownPushPullSelected, setDropDownPushPullSelected] = useState(PushPullEnum.Push)
function handlePlanHeader(date: DateData) {
const s: string = ("Plan " + date.day + "-" + date.month + "-" + date.year)
SetDialogState({ ...dialogState, planHeader: s })
}
useEffect(() => {
let tempExercises: Exercise[]
if (exerciseState.exercises[0] != undefined)
if (exerciseState.exercises[0].name == ""
|| exerciseState.exercises.length <= 0)
retrieveExercises((_, r) => {
tempExercises = r.rows._array
tempExercises.forEach(ex => ex.major_muscles = initialMajorMuscles)
if (scheduledItemState.scheduledItems[0] != undefined)
if (scheduledItemState.scheduledItems[0].exercise
== initialExerciseState.aExercise)
retrieveScheduledItems(
(_, results) => {
const tempScheduledItems: ScheduledItem[] = results.rows._array
const a = results.rows._array.slice()
tempScheduledItems.forEach((ms, index) => {
ms.date = JSON.parse(ms.date.toString())
const t = tempExercises.find(ex => ex.name == a[index].exercise)
tempScheduledItems[index].exercise = t!
})
setScheduledItemState({
...scheduledItemState,
scheduledItems: tempScheduledItems,
filteredScheduledItems: tempScheduledItems
})
})
setExerciseState({
...exerciseState, exercises: tempExercises,
filteredExercises: tempExercises
})
})
if (exerciseState.majorMuscles[0] == initialMajorMuscles[0]) {
retrieveMajorMuscles(
(_, results) => setExerciseState({
...exerciseState,
majorMuscles: results.rows._array
}))
}
if (emm[0] == initialEmm[0])
retrieveExerciseMajorMuscleRelationships(
(_, results) => setEmm(results.rows._array))
if (exerciseState.majorMuscles.length > 1
&& exerciseState.exercises.length > 1
&& emm.length > 1) {
emm.forEach(x => {
const ex: Exercise =
exerciseState.exercises.find(e => e.name == x.exercise_name)!
const mm2: MajorMuscle = exerciseState
.majorMuscles
.find(mm => mm.name == x.major_muscle_name)!
if (ex == undefined || mm2 == undefined) return
if (ex.major_muscles == initialMajorMuscles)
ex.major_muscles = [mm2!]
else if (!ex.major_muscles.find(x => x.name == mm2.name))
ex.major_muscles.push(mm2!)
})
}
}, [scheduledItemState, exerciseState, emm])
init()
let textInputStyle, numberInputStyle
if (dialogState.isEditable) {
textInputStyle = styles.textInputEditable
numberInputStyle = styles.numberInputEditable
} else {
textInputStyle = styles.textInputViewOnly
numberInputStyle = styles.numberInputViewOnly
}
const handleResetDB = () => {
resetTables()
setExerciseState(initialExerciseState)
setScheduledItemState(initialScheduledItemState)
SetDialogState(initialDialogState)
setEmm(initialEmm)
}
//Android only export and import
async function handleExport() {
const exportData = {
exercises: exerciseState.exercises,
scheduledItems: scheduledItemState.scheduledItems
}
const permissions = await StorageAccessFramework.requestDirectoryPermissionsAsync()
if (!permissions.granted) return
let uri = await StorageAccessFramework.createFileAsync(
permissions.directoryUri, "GymTracker backup", "application/json"
)
await StorageAccessFramework.writeAsStringAsync(uri, JSON.stringify(exportData))
Alert.alert("", `The backup data is now saved in the approved folder as `
+ `GymTracker backup.json`
+ `\nNote: if there is multiple backups, it will be numbered.`,
[{
text: "Cancel",
onPress: () => { },
style: "cancel"
}],
{ cancelable: true }
)
}
async function handleImport() {
Toast.show("Please grant folder permissions.")
const permissions =
await StorageAccessFramework
.requestDirectoryPermissionsAsync()
if (!permissions.granted) return
Toast.show("Please choose the correct json file")
let documentResult: DocumentPicker.DocumentResult =
await DocumentPicker.getDocumentAsync({ type: 'application/json' })
if (documentResult.type == 'cancel') return
const data = JSON.parse(
await StorageAccessFramework
.readAsStringAsync(documentResult.uri))
console.log(data)
if (!('exercises' in data) || !('scheduledItems' in data)) {
Toast.show("The data does not have the correct format")
return
}
if (!Array.isArray(data.exercises)
|| !Array.isArray(data.scheduledItems)) {
Toast.show("The data does not have the correct format")
return
}
deleteFromExerciseAndScheduledItem()
setExerciseState({
...exerciseState,
exercises: data.exercises,
filteredExercises: data.exercises
})
setScheduledItemState({
...scheduledItemState,
scheduledItems: data.scheduledItems,
filteredScheduledItems: data.scheduledItems
})
data.exercises.forEach((ex: Exercise) => createExercise(ex))
console.log(scheduledItemState.scheduledItems)
data
.scheduledItems
.forEach((si: ScheduledItem) => createScheduledItem(si))
}
function cancelDialog() {
SetDialogState({
...dialogState,
isExDialogVisible: false,
isCalendarDialogVisible: false,
isPlanDialogVisible: false
})
}
// Exercises Functions:
//renders
const renderExerciseDialogForViewing = (exercise: Exercise) => {
textInputStyle = styles.textInputViewOnly
setExerciseState({
...exerciseState,
aExercise: exercise,
oldExerciseName: exercise.name
})
const names: string[] = []
exercise.major_muscles.forEach(mm => names.push(mm.name))
setDropDownMajorMuscleValues(names)
setDropDownPushPullSelected(exercise.push_or_pull)
SetDialogState({
...dialogState,
isExDialogVisible: true, openPushPullDropDown: false,
dialogText: ExerciseInformationText,
isEditable: false, isDropDownOpen: false,
})
}
const renderExerciseDialogForEdit = () => {
textInputStyle = styles.textInputEditable
SetDialogState({
...dialogState,
isEditable: true,
dialogText: EditExerciseText,
isDropDownOpen: false,
openPushPullDropDown: false
})
}
const renderExerciseDialogForCreate = () => {
setExerciseState({
...exerciseState, aExercise:
initialExerciseState.aExercise
})
setDropDownMajorMuscleValues([])
textInputStyle = styles.textInputEditable
SetDialogState({
...dialogState,
isExDialogVisible: true, openPushPullDropDown: false,
dialogText: CreateExerciseText, isEditable: true, isDropDownOpen: false
})
setDropDownPushPullSelected(PushPullEnum.Push)
}
//db
const commonExercisesCRUD = (es: Exercise[]) => {
setExerciseState({
...exerciseState, exercises: [...es],
filteredExercises: [...es],
filteredExerciseKeyword: ""
})
cancelDialog()
}
const deleteExerciseConfirmation = (exercise: Exercise) => {
Alert.alert(
"Confirmation",
"Are you sure you want to delete this exercise?",
[{
text: "Yes",
onPress: () => deleteExerciseWithStateUpdate(exercise)
},
{
text: "No",
onPress: () => renderExerciseDialogForViewing(exercise)
}],//warning, recursive-
{ cancelable: true }
)
}
const deleteExerciseWithStateUpdate = (exercise: Exercise) => {
const selected: MajorMuscle[] = exerciseState.majorMuscles.filter(
x => dropDownMajorMuscleNameSelected.includes(x.name))
selected.forEach(
x => deleteExerciseMajorMuscleRelationship(exercise.name, x.name))
deleteExercise(exercise.name, () => {
const deletedName = exercise.name
const es: Exercise[] = exerciseState.exercises.slice()
es.forEach((currentExercise, i) => {
if (currentExercise.name == deletedName) {
es.splice(i, 1)
return
}
})
//correct way of removing element from a array for me. Not using delete keyword which leaves a undefined space
Toast.show("The exercise " + deletedName + " has is deleted.")
commonExercisesCRUD(es)
})
}
const updateExerciseWithStateUpdate = () => {
const aExercise = exerciseState.aExercise
if (aExercise.name.trim() == "") {
Toast.show("name cannot be empty")
return
}
const oldExerciseName = exerciseState.oldExerciseName
const pushPullDropDownValue = dropDownPushPullSelected
const selected: MajorMuscle[] = exerciseState.majorMuscles.filter(
x => dropDownMajorMuscleNameSelected.includes(x.name))
const toBeCreated: MajorMuscle[] = selected.filter(
x => !aExercise.major_muscles.find(t => t.name == x.name))
const toBeDeleted: MajorMuscle[] = aExercise.major_muscles.filter(
x => !selected.find(t => t.name == x.name))
toBeCreated.forEach(
x => createExerciseMajorMuscleRelationship(aExercise.name, x.name))
toBeDeleted.forEach(
x => deleteExerciseMajorMuscleRelationship(aExercise.name, x.name))
aExercise.push_or_pull = pushPullDropDownValue
updateExercise(aExercise, oldExerciseName,
() => {
const exerciseToBeUpdated: Exercise = {
name: aExercise.name, description: aExercise.description,
imagesJson: aExercise.imagesJson,
major_muscles: selected, push_or_pull: dropDownPushPullSelected
}
const es: Exercise[] = exerciseState.exercises.slice()
es.forEach((currentExercise, i) => {
if (currentExercise.name == oldExerciseName) {
es.splice(i, 1, exerciseToBeUpdated)
return
}
})
commonExercisesCRUD(es)
Toast.show("The exercise is updated.")
})
}
function createExerciseWithStateUpdate() {
const aExercise = exerciseState.aExercise
if (aExercise.name == "") {
Toast.show("name cannot be empty")
return
}
const selected: MajorMuscle[] = exerciseState.majorMuscles.filter(
x => dropDownMajorMuscleNameSelected.includes(x.name))
selected.forEach(
x => createExerciseMajorMuscleRelationship(aExercise.name, x.name))
aExercise.push_or_pull = dropDownPushPullSelected
console.log(aExercise.push_or_pull)
createExercise(aExercise, (_, result) => {
const es: Exercise[] = exerciseState.exercises.slice()
es.push({
name: aExercise.name, description: aExercise.description,
imagesJson: aExercise.imagesJson, major_muscles: selected,
push_or_pull: dropDownPushPullSelected
})
commonExercisesCRUD(es)
Toast.show("The exercise " + aExercise.name + " is created.")
})
}
function handleFilterExercises(keyword: string) {
console.log("handleFilterExercises")
setExerciseState({
...exerciseState,
filteredExercises:
exerciseState.exercises.filter(e => (
e.name.includes(keyword)
|| e.major_muscles.filter(
mm => mm.name.includes(keyword)
).length > 0
|| e.description.includes(keyword)
)),
filteredExerciseKeyword: keyword
})
}
//Scheduled Item Functions:
//renders:
function renderScheduledItemDialogForViewing(scheduledItem: ScheduledItem) {
textInputStyle = styles.textInputViewOnly
numberInputStyle = styles.numberInputViewOnly
setScheduledItemState({
...scheduledItemState,
aScheduledItem: scheduledItem
})
SetDialogState({
...dialogState,
isEditable: false, dialogText: ScheduledItemInformation,
isExDialogVisible: false, openPushPullDropDown: false,
isPlanDialogVisible: true
})
setDropDownExNameSelected(scheduledItem.exercise.name)
}
function commonLogicForScheduledItemEditAndDuplication(dialogText: string) {
textInputStyle = styles.textInputEditable
setScheduledItemState({ ...scheduledItemState, })
SetDialogState({
...dialogState, isEditable: true,
isDropDownOpen: false, dialogText: dialogText
})
setDropDownExNameSelected(scheduledItemState.aScheduledItem.exercise.name)
}
function renderScheduledItemDialogForCreate() {
textInputStyle = styles.textInputEditable
numberInputStyle = styles.numberInputEditable
setScheduledItemState({
...scheduledItemState,
aScheduledItem: initialScheduledItem[0]
})
SetDialogState({
...dialogState,
isEditable: true, dialogText: CreateScheduledItemText,
isPlanDialogVisible: true, isDropDownOpen: false,
})
setDropDownExNameSelected(exerciseState.exercises[0].name)
const parts: string[] = dialogState.planHeader.split(" ")[1].split("-")
const monthNumber: number = Number(parts[1])
const month: string = monthNumber < 10
? "0" + monthNumber.toString()
: monthNumber.toString()
const day: string = Number(parts[0]) < 10 ? "0" + parts[0] : parts[0]
const date: DateData = {
year: Number(parts[2]), month: monthNumber,
day: Number(parts[0]), timestamp: 0,
dateString: parts[2] + "-" + month + "-" + day
}
const aScheduledItem = initialScheduledItem[0]
aScheduledItem.date = date
setScheduledItemState({
...scheduledItemState,
aScheduledItem: { ...aScheduledItem }
})
}
//db:
function commonScheduledItemCRUD(si: ScheduledItem[]) {
setScheduledItemState({
...scheduledItemState, scheduledItems: [...si],
filteredScheduledItems: [...si],
selectedScheduledItems: [],
isMovingScheduledItems: false
})
cancelDialog()
}
const deleteScheduledItemConfirmation = (ms: ScheduledItem) => {
Alert.alert(
"Confirmation",
"Are you sure you want to delete this scheduled item?",
[{ text: "Yes", onPress: () => deleteScheduledItemWithStateUpdate(ms.id) },
{ text: "No", onPress: () => renderScheduledItemDialogForViewing(ms) }],//warning, recursive
{ cancelable: true }
)
}
const deleteScheduledItemWithStateUpdate = (id: number) => {
deleteScheduledItem(id, () => {
const si = scheduledItemState.scheduledItems.slice()
si.forEach((ms1, i) => {
if (ms1.id == id) {
si.splice(i, 1)
return
}
}) //correct way of removing element from a array for me. Not using delete keyword which leaves a undefined space
Toast.show("The scheduled item is deleted.")
commonScheduledItemCRUD(si)
})
}
const updateScheduledItemWithStateUpdate = () => {
const aScheduledItem = scheduledItemState.aScheduledItem
if (dropDownExNameSelected == undefined || dropDownExNameSelected == "") {
Toast.show("exercise must be selected")
return
}
const theexercise = exerciseState.exercises.filter((e, i, a) => {
if (e.name == dropDownExNameSelected) return e
})[0]
updateScheduledItem(aScheduledItem,
(_, result) => {
const toBeUpdated: ScheduledItem = {
id: aScheduledItem.id, exercise: theexercise, reps: aScheduledItem.reps,
percent_complete: aScheduledItem.percent_complete, sets: aScheduledItem.sets,
duration_in_seconds: aScheduledItem.duration_in_seconds,
weight: aScheduledItem.weight, notes: aScheduledItem.notes,
date: aScheduledItem.date
}
const ms: ScheduledItem[] = scheduledItemState.scheduledItems.slice()
ms.forEach((currentScheduledItem, i) => {
if (currentScheduledItem.id == aScheduledItem.id) {
ms.splice(i, 1, toBeUpdated)
return
}
})
commonScheduledItemCRUD(ms)
Toast.show("The major set is updated.")
},
)
}
function createScheduledItemWithStateUpdate() {
const aScheduledItem = scheduledItemState.aScheduledItem
exerciseState.exercises.forEach(e => {
if (e.name == dropDownExNameSelected) {
aScheduledItem.exercise = e
return
}
})
createScheduledItem(aScheduledItem,
(_, r) => {
const tempScheduledItem = { ...aScheduledItem }
tempScheduledItem.id = r.insertId!
tempScheduledItem.date = aScheduledItem.date
tempScheduledItem.duration_in_seconds = aScheduledItem.duration_in_seconds
const m = scheduledItemState.scheduledItems.slice()
m.push(tempScheduledItem)
commonScheduledItemCRUD(m)
Toast.show("Scheduled item created.")
})
}
function handleFilterScheduledItem(keyword: string) {
console.log("here")
const filtered = scheduledItemState.scheduledItems.filter(si => {
const sec = si.duration_in_seconds % 60
const min = Math.floor(si.duration_in_seconds / 60)
return ((si.percent_complete.toString() + "%").includes(keyword)
|| si.id.toString().includes(keyword)
|| (si.weight.toString() + "kg").includes(keyword)
|| (si.sets.toString() + "x" + si.reps.toString()).includes(keyword)
|| (min + " minutes").includes(keyword)
|| (sec + " seconds").includes(keyword)
|| si.exercise.name.includes(keyword)
|| si.exercise.major_muscles.filter(
si => si.name.includes(keyword)
).length > 0
)
}
)
setScheduledItemState({
...scheduledItemState,
filteredScheduledItems: filtered,
filteredScheduledItemKeyword: keyword
})
}
const buttonsSetProps: ButtonSetProps = {
cancelDialog: cancelDialog,
deleteScheduledItemConfirmation: deleteScheduledItemConfirmation,
deleteExerciseConfirmation: deleteExerciseConfirmation,
createExerciseWithStateUpdate: createExerciseWithStateUpdate,
updateExerciseWithStateUpdate: updateExerciseWithStateUpdate,
createScheduledItemWithStateUpdate: createScheduledItemWithStateUpdate,
updateScheduledItemWithStateUpdate: updateScheduledItemWithStateUpdate,
renderScheduledItemDialogForViewing: renderScheduledItemDialogForViewing,
renderScheduledItemDialogForDuplication:
() => commonLogicForScheduledItemEditAndDuplication(DuplicateScheduledItemText),
renderScheduledItemDialogForEdit:
() => commonLogicForScheduledItemEditAndDuplication(EditScheduledItemText),
renderExerciseDialogForEdit: renderExerciseDialogForEdit,
renderExerciseDialogForViewing: renderExerciseDialogForViewing,
}
const contextProps: ContextProps = {
renderScheduledItemDialogForViewing: renderScheduledItemDialogForViewing,
renderScheduledItemDialogForCreate: renderScheduledItemDialogForCreate,
renderExerciseDialogForCreate: renderExerciseDialogForCreate,
renderExerciseDialogForViewing: renderExerciseDialogForViewing,
handleFilterScheduledItem: handleFilterScheduledItem,
handlePlanHeader: handlePlanHeader,
handleFilterExercises: handleFilterExercises,
commonScheduledItemCRUD: commonScheduledItemCRUD,
scheduledItemState: scheduledItemState,
dialogState: dialogState,
exerciseState: exerciseState,
setDialogState: SetDialogState,
setScheduledItemState: setScheduledItemState,
setExerciseState: setExerciseState,
}
return (
<>
<NavigationContainer>
<ScheduleDialog
dialogState={dialogState}
exerciseState={exerciseState}
scheduledItemState={scheduledItemState}
dropDownExNameSelected={dropDownExNameSelected}
setScheduledItemState={setScheduledItemState}
setDialogState={SetDialogState}
setDropDownExNameSelected={setDropDownExNameSelected}
createExerciseWithStateUpdate={createExerciseWithStateUpdate}
updateExerciseWithStateUpdate={updateExerciseWithStateUpdate}
buttonsSetProps={buttonsSetProps}
/>
<ExerciseDialog
exerciseState={exerciseState}
dialogState={dialogState}
scheduledItemState={scheduledItemState}
majorMuscles={exerciseState.majorMuscles}
dropDownPushPullSelected={dropDownPushPullSelected}
dropDownMajorMuscleNameSelected={dropDownMajorMuscleNameSelected}
setDropDownPushPullSelected={setDropDownPushPullSelected}
setDialogState={SetDialogState}
setExerciseState={setExerciseState}
setMajorMuscleValues={setDropDownMajorMuscleValues}
buttonsSetProps={buttonsSetProps}
/>
<SelectDateDialog
scheduledItemState={scheduledItemState}
dialogState={dialogState}
setScheduledItemState={setScheduledItemState}
setDialogState={SetDialogState}
commonScheduledItemCRUD={commonScheduledItemCRUD}
buttonsSetProps={buttonsSetProps}
/>
<SettingsScreenContext.Provider value={{
handleResetDB: handleResetDB,
handleExport: handleExport,
handleImport: handleImport
}}>
<ExerciseScreenContext.Provider value={{ contextProps: contextProps }}>
<ScheduledItemContext.Provider value={{ contextProps: contextProps }}>
<Tab.Navigator
screenOptions={({ route }) =>
({
tabBarIcon: ({ focused, color, size }) => {
let iconName: any
switch (route.name) {
case 'Plan':
iconName = focused ? 'clipboard-list' : 'clipboard-list-outline'
return <MaterialCommunityIcons name={iconName} size={size} color={color} />
case 'Exercises':
iconName = focused ? 'arm-flex' : 'arm-flex-outline'
return <MaterialCommunityIcons name={iconName} size={size} color={color} />
case 'Settings':
iconName = focused ? 'settings' : 'settings-outline'
return <Ionicons name={iconName} size={size} color={color} />
}
},
tabBarActiveTintColor: Colors.light.tint
})
}>
<Tab.Screen
options={{ headerTitle: dialogState.planHeader }}
name="Plan" component={PlanScreen} />
<Tab.Screen name="Exercises" component={ExercisesScreen} />
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
</ScheduledItemContext.Provider>
</ExerciseScreenContext.Provider>
</SettingsScreenContext.Provider>
</NavigationContainer >
</>
)
}
ExerciseScreen.tsx
import React from "react"
import { FlatList, Text, View, TouchableOpacity } from 'react-native'
import { ExerciseScreenContext } from '../constants/initialValues'
import { useContext } from 'react'
import layoutConstants from '../constants/Layout'
import { Exercise } from "../types"
import Layout from "../constants/Layout"
import Colors from "../constants/Colors"
import { TextInput } from "react-native"
import { Ionicons } from "#expo/vector-icons"
import { styles } from "../constants/styles"
function ExercisesScreen() {
let c = useContext(ExerciseScreenContext)
let filteredExercises: Exercise[] = c.contextProps.exerciseState.filteredExercises
let filteredKeyword: string = c.contextProps.exerciseState.filteredExerciseKeyword
let handleSelected: Function = c.contextProps.renderExerciseDialogForViewing
let handleShowCreate: Function = c.contextProps.renderExerciseDialogForCreate
let handleFilterExercises: Function = c.contextProps.handleFilterExercises
return (
<View style={{
flexDirection: "column", flex: 1,
alignItems: 'flex-start',
justifyContent: 'center'
}}>
<FlatList
style={{ width: '100%', transform: [{ rotateX: "180deg" }], }}
data={filteredExercises}
initialNumToRender={15}
renderItem={
({ item }) =>
<TouchableOpacity style={{
alignItems: "flex-start",
padding: layoutConstants.defaultMargin,
}}
onPress={() => handleSelected(item)}
>
<Text style={{
transform: [{ rotateX: "180deg" }],
fontSize: layoutConstants.defaultFontSize
}}>
{item.name}
</Text >
</TouchableOpacity>
}
/>
<TextInput
style={styles.filterExercisesTextInput}
placeholder="Type here to filter exercises..."
onChange={text => handleFilterExercises(text.nativeEvent.text)}
value={filteredKeyword}
/>
<View style={{ bottom: 20, end: 20, position: 'absolute' }}>
<TouchableOpacity
style={{
...styles.planScreenPressable,
backgroundColor: Colors.light.tint,
}}
onPress={() => handleShowCreate()}
>
<Ionicons style={{ bottom: "-5%", right: "-10%" }}
name="add-outline"
size={Layout.defaultMargin + 40} color="white" />
</TouchableOpacity>
</View>
</View>
)
}
export { ExercisesScreen }
Edit:
Link to my repo
https://github.com/themissingCRAM/GymTracker
I guess i am not very clear in my question either the list of exercises should be there on my ExerciseScreen on startup but it isn't when I use my apk version only.
And my apologies, I am new to React and RN.
Faced a problem while using react-native-fbsdk-next.
The first call to Profile.getCurrentProfile on android returns null. Everything works correctly on ios. If authorization is then called again, the user profile is returned correctly.
If you log out and then try to log in, the first attempt will return null.
Please tell me how to fix this?
const platformPermissions =
Platform.OS === 'ios'
? [['public_profile', 'email'], 'limited', 'my_nonce']
: [['public_profile', 'email']];
const accessToken = yield LoginManager.logInWithPermissions(
...platformPermissions,
).then(
async response => {
if (response.isCancelled) {
return;
}
if (Platform.OS === 'ios') {
const result = await AuthenticationToken.getAuthenticationTokenIOS();
return result?.authenticationToken;
}
if (Platform.OS === 'android') {
const result = await AccessToken.getCurrentAccessToken();
return result?.accessToken;
}
},
error => {},
);
if (accessToken) {
const currentProfile = yield Profile.getCurrentProfile(
profile => profile,
);
if (currentProfile) {
const userData =
Platform.OS === 'ios'
? {
...currentProfile,
firstName: currentProfile.name.split(' ')[0] || null,
lastName: currentProfile.name.split(' ')[1] || null,
}
: currentProfile;
const {data} = yield Axios.post(ROUTES.AUTH.FACEBOOK_SIGN, {
...userData,
accessToken,
});
yield put(setCurrentUser({user: data}));
yield put(setMethod({method: 'facebook'}));
}
}
The issue is in the library itself. PR needs to be created for the solution. Checkout the progress at https://github.com/thebergamo/react-native-fbsdk-next/issues/73
I was facing this issue with the LoginButton. I have no issue when using LoginManager with a custom Button.
My Custom Login button:
const FBLoginButton = () => {
const nav = useNavigation();
return (
<Button
title="Login with Facebook"
onPress={() => {
LoginManager.logInWithPermissions(["public_profile"]).then(
function (result) {
if (result.isCancelled) {
console.log("Login cancelled");
} else {
console.log("Success");
nav.navigate("Home"); // Go to Home screen
}
},
function (error) {
console.log("Login fail with error: " + error);
}
);
}}
/>
);
};
My Home Screen
import { Profile } from "react-native-fbsdk-next";
const HomeScreen = () => {
const [currentUser, setCurrentUser] = useState(null);
const nav = useNavigation();
useEffect(() => {
Profile.getCurrentProfile().then((profile) => {
if (profile) {
setCurrentUser(profile);
}
});
}, []);
return (
<View>
{currentUser ? (
<>
<Text>{`id: ${currentUser.userID}, name: ${currentUser.name}`}</Text>
<Button
title="Logout"
onPress={() => {
LoginManager.logOut();
nav.navigate("Login");
}}
/>
</>
) : (
<Text>Loading...</Text>
)}
</View>
);
};
I have install the react native firebase notificaiton ,notifications are working fine but when i try to send custom sound it wont work.
I find out that on Android version > 7 we have to create firebase channel in order to play custom sound but i dont know how to and where to create notification channel.
I have created two files inside the src folder of the root folder of my project where i handle notificatons.
1.LocalNotificationService.js
2.FcmService.js
Code for LocalNotificationService.js
import PushNotification from 'react-native-push-notification';
import {Platform} from 'react-native';
// import { notifications } from "react-native-firebase";
class LocalNotificationService {
configure = (onOpenNotification) => {
PushNotification.configure({
onRegister: function (token) {
console.log('[LocalNotificationService] onRegister', token);
},
onNotification: function (notification) {
console.log('[LocalNotificationService] onNotification', notification);
if (!notification?.data) {
return;
}
notification.userInteraction = true;
onOpenNotification(
Platform.OS === 'ios' ? notification.data.item : notification.data,
);
//Only call callback if not from foreground
if (Platform.OS === 'ios') {
notification.finish(PushNotificationIOS.FetchResult.NoData);
}
},
// 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,
});
};
unregister = () => {
PushNotification: this.unregister();
};
showNotification = (id, title, message, data = {}, options = {}) => {
PushNotification.localNotification({
...this.buildAndroidNotification(id, title, message, data, options),
title: title || '',
message: message || '',
playSound: options.playSound || true,
soundName: options.soundName || 'default',
// sound:'test',
userInteracsstion: false, /// boolean if the condition was opened by the user from the notification
});
};
buildAndroidNotification = (id, title, message, data = {}, options = {}) => {
return {
id: id,
// sound:"test",
autoCancel: true,
largeIcon: options.largeIcon || 'icon',
smallIcon: options.smallIcon || 'test_icon',
bigText: message || '',
subText: title || '',
color: 'green',
vibrate: options.vibrate || true,
vibration: options.vibration || 1000,
priority: options.priority || ' high',
importance: options.importance || 'high', // (options) set notification importance , default high
// data : data,
};
};
cancelAllLocalNotification = () => {
if (Platform.OS === 'ios') {
PushNotificationIOS.removedAllDeliveredNotification();
} else {
PushNotification.cancelAllLocalNotification();
}
};
removedDeliveredNotificationById = (notificationId) => {
console.log(
'[LocalNotificationService] removeDeliveredNotificationById: ',
notificationId,
);
PushNotification.cancelLocalNotfication({id: `${notificationId}`});
};
}
export const localNotificationService = new LocalNotificationService();
Code for FcmService.js
import messaging from '#react-native-firebase/messaging'
// import type {Notification,NotificationOpen} from 'react-native-firebase'
import { Platform } from 'react-native'
class FCMService {
register = (onRegister,onNotification,onOpenNotification) => {
this.checkPermission(onRegister)
this.createNotificationListeners(onRegister,onNotification,onOpenNotification)
}
registerAppWithFCM = async() => {
if(Platform.OS === "ios"){
await messaging().registerDeviceForRemoteMessages();
await messaging().setAutoInitEnabled(true)
}
}
checkPermission = (onRegister) => {
messaging().hasPermission()
.then(enabled => {
if(enabled) {
//User has permission
this.getToken(onRegister)
}
else{
//User Dont have permission
this.requestPermission(onRegister)
}
}).catch(error => {
console.log("Permission Rejected",error)
})
}
getToken = (onRegister) => {
messaging().getToken()
.then(fcmToken => {
if(fcmToken) {
onRegister(fcmToken)
}
else{
console.log("User Dont ave a Device Token")
}
}).catch(error => {
console.log("get token rejected", error)
})
}
requestPermission = (onRegister) => {
messaging().requestPermission()
.then(() => {
this.getToken(onRegister)
}).catch(error => {
console.log("Request Permission Rejected",error)
})
}
deleteToken = () => {
console.log("Delete Token")
messaging().deleteToken()
.catch(error => {
console.log("Delete token error",error)
})
}
createNotificationListeners = (onRegister, onNotification,onOpenNotification) => {
messaging().onNotificationOpenedApp(remoteMessage => {
console.log("onNotificationOpenedApp notification caused to open app")
if(remoteMessage){
const notification = remoteMessage.notification
onOpenNotification(notification)
}
});
//When the application is opened from a quite state.
messaging().getInitialNotification().then(remoteMessage => {
console.log("getInitialNotification notification caused to open app")
if(remoteMessage){
const notification = remoteMessage.notification
onOpenNotification(notification)
}
})
//Foreground state messages
this.messageListner = messaging().onMessage(async remoteMessage => {
console.log("A new FCM Message Arrived",remoteMessage)
if(remoteMessage){
let notification = null
if(Platform.OS === "ios") {
notification = remoteMessage.data.notification
}else{
notification = remoteMessage.notification
}
onNotification(notification)
}
})
}
unRegister = () => {
}
export const fcmService = new FCMService()
App.js
import React , {useEffect,useState} from 'react'
import {View,StyleSheet,Text,Button,TextInput} from 'react-native'
import {fcmService} from './src/FCMService'
import {localNotificationService} from './src/LocalNotificationService'
import auth from '#react-native-firebase/auth';
export default function App() {
useEffect(() => {
fcmService.registerAppWithFCM()
fcmService.register(onRegister,onNotification,onOpenNotification)
localNotificationService.configure(onOpenNotification)
function onRegister(token) {
console.log("[App] onRegister: ",token)
setTokenwa(token)
}
function onNotification(notify) {
console.log("[App] onNotification: ",notify)
const options = {
soundName : 'test',
playSound : true
}
localNotificationService.showNotification(
0,
notify.title,
notify.body,
notify,
options
)
}
function onOpenNotification(notify) {
console.log("[App] onOpenNotification : " , notify)
alert("Open Notification" + notify.body)
}
return () => {
console.log("[App] unRegister")
fcmService.unRegister()
localNotificationService.unregister()
}
}, [])
const [confirm, setConfirm] = useState(null);
const [code, setCode] = useState('');
const [tokenwa, setTokenwa] = useState('');
// Handle the button press
async function signInWithPhoneNumber(phoneNumber) {
const confirmation = await auth().signInWithPhoneNumber(phoneNumber);
setConfirm(confirmation);
}
async function confirmCode() {
try {
console.log("code send")
await confirm.confirm(code);
} catch (error) {
console.log('Invalid code.');
}
}
if (!confirm) {
return (
<Button
title="Phone Number Sign In"
onPress={() => signInWithPhoneNumber('+91 7769948296')}
/>
);
}
return (
<>
<Text>{tokenwa}</Text>
<View style={styles.container}>
<Text>Sample React Native Firebase </Text>
<Button title="Press Me" onPress={() => localNotificationService.cancelAllLocalNotification}></Button>
<TextInput value={code} placeholder="test" onChangeText={text => setCode(text)} />
<Button title="Confirm Code" onPress={() => confirmCode()} />
</View>
</>
)
}
const styles = StyleSheet.create({
container : {
flex : 1,
alignItems : 'center',
justifyContent : 'center'
}
})