2 - React Native
2.1 Create a new React Native app (I used Expo)
Instructions in the official documentation
2.2 Install the dependencies
check the dependencies I used (package.json)
{
"name": "YOUR APP NAME",
"version": "1.0.0",
"main": "expo-router/entry",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"@react-native-community/netinfo": "11.3.1",
"axios": "^1.7.2",
"expo": "~51.0.11",
"expo-constants": "~16.0.2",
"expo-linking": "~6.3.1",
"expo-router": "~3.5.15",
"expo-status-bar": "~1.12.1",
"laravel-echo": "^1.16.1",
"pusher-js": "^8.4.0-rc2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.74.2",
"react-native-safe-area-context": "4.10.1",
"react-native-screens": "3.31.1",
"react-native-web": "~0.19.10",
"socket.io-client": "^4.7.5",
"twrnc": "^4.2.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"react-native-dotenv": "^3.4.11"
},
"private": true
}
check this to configure react-native-dotenv
I just made this (babel.config.js):
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: [
["module:react-native-dotenv"],
// {
// envName: "APP_ENV",
// moduleName: "@env",
// path: ".env",
// blocklist: null,
// allowlist: null,
// blacklist: null, // DEPRECATED
// whitelist: null, // DEPRECATED
// safe: false,
// allowUndefined: true,
// verbose: false,
// },
],
};
};
2.3 Setup .env
.env
# YOUR-SERVER-HOST:YOUR-SERVER-PORT
# (YOU CAN GET IT WHEN YOU RUN php artisan server --host=YOUR-LOCAL-IP)
BACKEND_API_URL=http://YOUR-SERVER-HOST:YOUR-SERVER-PORT/api
REVERB_APP_ID= # FIND IT IN .env FILE OF YOU LARAVEL APPLICATION
REVERB_APP_KEY= # FIND IT IN .env FILE OF YOU LARAVEL APPLICATION
REVERB_APP_SECRET= # FIND IT IN .env FILE OF YOU LARAVEL APPLICATION
REVERB_HOST= # FIND IT IN .env FILE OF YOU LARAVEL APPLICATION
REVERB_PORT= # FIND IT IN .env FILE OF YOU LARAVEL APPLICATION
REVERB_SCHEME= # FIND IT IN .env FILE OF YOU LARAVEL APPLICATION
2.4 Setup services (API)
services/api
import axios from "axios";
import { BACKEND_API_URL } from "@env";
const api = axios.create({
baseURL: BACKEND_API_URL,
headers: {
"Content-Type": "application/json",
},
});
const setBearerToken = (token) => {
api.defaults.headers.common["Authorization"] = `Bearer ${token}`;
};
export { api, setBearerToken };
services/auth
import { api } from "./api";
export const Auth = {
async login(email, password) {
const options = {
method: "POST",
url: "/login",
data: {
email,
password,
},
};
try {
const response = await api.request(options);
return response.data;
} catch (error) {
throw error;
}
},
};
2.5 Create Echo instance (our websocket client)
hooks/echo
import { useEffect, useState } from "react";
// import axios from "axios";
import { api as axios } from "../services/api";
import Echo from "laravel-echo";
import Pusher from "pusher-js/react-native";
import { REVERB_APP_KEY, REVERB_HOST, REVERB_PORT, REVERB_SCHEME } from "@env";
const useEcho = () => {
const [echoInstance, setEchoInstance] = useState(null);
useEffect(() => {
// Setup Pusher client
const PusherClient = new Pusher(REVERB_APP_KEY, {
wsHost: REVERB_HOST,
wsPort: REVERB_PORT ?? 80,
wssPort: REVERB_PORT ?? 443,
forceTLS: (REVERB_SCHEME ?? "https") === "https",
enabledTransports: ["ws", "wss"],
disableStats: true,
cluster: "mt1",
authorizer: (channel, options) => {
return {
authorize: (socketId, callback) => {
axios
.post("/broadcasting/auth", {
socket_id: socketId,
channel_name: channel.name,
})
.then((response) => {
callback(false, response.data);
})
.catch((error) => {
callback(true, error);
});
},
};
},
});
// Create Echo instance
const echo = new Echo({
broadcaster: "reverb",
client: PusherClient,
});
setEchoInstance(echo);
// Cleanup on unmount
return () => {
if (echo) {
echo.disconnect();
}
};
}, []);
return echoInstance;
};
export default useEcho;
2.6 Create chat component
components/chatInterface
// src/components/ChatInterface.js
import React, { useState, useEffect } from "react";
import {
SafeAreaView,
View,
TextInput,
TouchableOpacity,
Text,
FlatList,
KeyboardAvoidingView,
Platform,
} from "react-native";
import { style as tw } from "twrnc";
import useEcho from "../hooks/echo";
import { api } from "../services/api";
const ChatInterface = ({ user }) => {
const [messages, setMessages] = useState([]);
const [inputText, setInputText] = useState("");
const [userData, setUserData] = useState(user);
const echo = useEcho();
const handleSend = async () => {
if (inputText.trim()) {
setInputText("");
const options = {
method: "POST",
url: "/send-message",
data: {
/**I logged in with user 1 and 7.
* then you configure this to be dynamic
* (giving the user the power to choose who they want to chat with)
* */
user_id: userData.id === 1 ? 7 : 1, // receiver
from: userData.id,
message: inputText,
},
};
try {
const response = await api.request(options);
if (response.status === 200) {
const data = response.data.data;
console.log("response", data);
setMessages((prevMessages) => [
...prevMessages,
{
id: data?.message?.id,
from: userData.id,
message: data?.message?.message,
},
]);
}
} catch (error) {
console.error(error);
// Handle error appropriately
}
}
};
function subscribeToChatChannel() {
if (echo) {
echo.private(`chat.${user?.id}`).listen("MessageSent", (e) => {
console.log("real-time-event", {
id: e.message?.id,
message: e.message?.message,
from: e.message?.from,
});
setMessages((prevMessages) => [
...prevMessages,
{
id: e.message?.id,
message: e.message?.message,
from: e.message?.from,
},
]);
});
}
}
useEffect(() => {
if (echo) {
console.log("user", userData.id);
subscribeToChatChannel();
}
return () => {
if (echo && user) {
echo.leaveChannel(`chat.${user?.id}`);
}
};
}, [echo, user]);
const renderItem = ({ item }) => (
<View
style={tw(
`flex flex-row my-2`,
item.from == userData.id ? `justify-end` : "justify-start"
)}
>
<View
style={tw(
`rounded-lg p-6 max-w-3/4`,
item.from == userData.id ? "bg-[#1EBEA5]" : "bg-[#8696a0]"
)}
>
<Text style={tw`text-white`}>{item.message}</Text>
</View>
</View>
);
return (
<SafeAreaView style={tw`flex-1 bg-[#0b141a]`}>
<FlatList
data={messages}
renderItem={renderItem}
keyExtractor={(item) => item.id}
contentContainerStyle={tw`p-4`}
/>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<View style={tw`p-4 border border-gray-700 gap-2`}>
<View style={tw`bg-blue-500 p-6 rounded-lg`}>
<Text style={tw`text-white`}>{userData?.name}</Text>
</View>
</View>
<View style={tw`flex flex-row p-4 border-t border-gray-700 gap-2`}>
<TextInput
style={tw`flex-1 bg-gray-800 text-white p-6 rounded-lg`}
value={inputText}
onChangeText={setInputText}
placeholder="Type a message..."
placeholderTextColor="gray"
/>
<TouchableOpacity
onPress={handleSend}
style={tw`bg-blue-500 p-6 rounded-lg`}
>
<Text style={tw`text-white`}>Send</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
export default ChatInterface;
2.7 Login users
index.js
import { StatusBar } from "expo-status-bar";
import { Text, TouchableOpacity, View } from "react-native";
import { style as tw } from "twrnc";
import ChatInterface from "../components/chatInterface";
import { useState } from "react";
import { setBearerToken } from "../services/api";
import { Auth } from "../services/auth";
export default function App() {
const [userData, setUserData] = useState(null);
async function handleLogin(userCredentials) {
try {
const response = await Auth.login(...userCredentials);
if (response.status == 200) {
const data = response.data;
setBearerToken(data.token);
setUserData(data.user);
}
} catch (error) {
console.error("Error:", error);
}
}
return userData ? (
<ChatInterface user={userData} />
) : (
<View style={tw(`bg-gray-300 flex-1 items-center justify-center gap-16`)}>
<TouchableOpacity
onPress={() =>
handleLogin(["FIRST-USER-EMAIL-HERE", "FIRST-USER-PASSWORD-HERE"])
}
style={tw`w-80 bg-blue-500 p-7 rounded-lg`}
>
<Text style={tw`text-white`}>login 1</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() =>
handleLogin(["SECOND-USER-EMAIL-HERE", "SECOND-USER-PASSWORD-HERE"])
}
style={tw`w-80 bg-blue-500 p-7 rounded-lg`}
>
<Text style={tw`text-white`}>login 2</Text>
</TouchableOpacity>
</View>
);
}
3 - Notes
One of the bases was this video on YouTube
Feel free to ask, agree or complain anything