Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MagicBell Mobile Inbox

This repo contains an open source mobile client for the MagicBell API, build in React Native. You can use it as an example project on how to setup a React Native app that integrates with MagicBell notifications and push notifications via APNs and FCM.
This repo contains an open source mobile client for the MagicBell API, built in React Native. You can use it as an example project on how to setup a React Native app that integrates with MagicBell notifications and push notifications via APNs and FCM.

To explore the full feature set of MagicBell, and to dive deeper into the API please refer to the [documentation](https://www.magicbell.com/docs).

Expand Down Expand Up @@ -31,7 +31,9 @@ In order to build the app you will need to have the native tool chains for the p

You will also need [NodeJS](https://nodejs.org) and [Yarn](https://yarnpkg.com) for obvious reasons.

More details on how to set up a React Native dev environment can be found on [reactnative.dev](https://reactnative.dev/docs/environment-setup).
More details on how to set up a React Native dev environment can be found on [reactnative.dev](https://reactnative.dev/docs/set-up-your-environment).

Install the dependencies by running `yarn`.

## Starting A Local Development Build

Expand Down Expand Up @@ -101,6 +103,26 @@ At this point you can use the keyboard shortcuts on the dashboard to build and o

If you want to launch the app on a specific simulator or even device, you can use the `shift+i`/`shift+a` shortcuts, or start another build process from the terminal while keeping Metro in the background by running `yarn ios` or `yarn android` (both of which support additional parameters that can be inspected by passing `-h`).

## Sending FCM Notifications
To send notifications with FCM, you will need a [Firebase account](https://firebase.google.com/) and an Android app. You can create the app by using the `Add app` button on your console and selecting android.
You should now see a button that says, `google-service.json`, using which you can download the `google-service.json` file.

If you have a pre-registered app then you can go into the Project Settings of the app, and in the General tab, you can find the button to download `google-service.json` in the Your apps section.

After downloading the file replace `google-service.json` file in the root of this project with your file.

To launch the Android app you can use:
```bash
yarn android:clean
```

The command will do a clean Android build and launch the Android app in an emulator.

For authentication, you will need a MagicBell userJWT, you can [generate it using your MagicBell API Key and the external ID of the user](https://www.magicbell.com/docs/api/authentication/user) you want to send notifications to.

To test if you are receiving notifications correctly, you can use the [FCM Test](https://www.magicbell.com/test/fcm).

You will need an Admin SDK private key, you can get it from your firebase console by going to the Project Settings by clicking on the gear button on the left sidebar. Then going to Service Accounts and clicking the `Generate new private key`, it will save a JSON file to your machine that you can then upload to the [MagicBell FCM Test](https://www.magicbell.com/test/fcm) page.

## Building Release Builds

Expand Down
29 changes: 5 additions & 24 deletions google-services.json
Original file line number Diff line number Diff line change
@@ -1,40 +1,21 @@
{
"project_info": {
"project_number": "371921703332",
"project_id": "react-native-starter-ee960",
"storage_bucket": "react-native-starter-ee960.firebasestorage.app"
"project_number": "922199420286",
"project_id": "mb-fcm-niya",
"storage_bucket": "mb-fcm-niya.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:371921703332:android:f0cc04373ce55917617131",
"mobilesdk_app_id": "1:922199420286:android:31042b682826f9fd4e172f",
"android_client_info": {
"package_name": "com.magicbell.mobileinbox"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyD4WB5GlZApHn66O1CM2r_z7z9Qiis_g_Y"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:371921703332:android:c46fd837b7edbe43617131",
"android_client_info": {
"package_name": "com.rnprototype"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyD4WB5GlZApHn66O1CM2r_z7z9Qiis_g_Y"
"current_key": "AIzaSyB3rwPM8HRM3iqzWar_vfsP5_oqQlK6-pc"
}
],
"services": {
Expand Down
4 changes: 1 addition & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { registerRootComponent } from 'expo';
import App from './src/App';

// Polyfills needed for @magicbell/react-headless
import EventSource from 'react-native-sse';
// Polyfills for React Native environment
import 'react-native-url-polyfill/auto';
global.EventSource = EventSource;

registerRootComponent(App);
13 changes: 0 additions & 13 deletions ios/ci_scripts/ci_post_clone.sh

This file was deleted.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@
"expo": "^52.0.27",
"expo-app-loading": "^2.1.1",
"expo-application": "~6.0.2",
"expo-dev-client": "~5.0.20",
"expo-font": "~13.0.3",
"expo-linking": "~7.0.4",
"expo-notifications": "~0.29.12",
"expo-splash-screen": "~0.29.21",
"lodash.isequal": "^4.5.0",
"magicbell-js": "^1.4.0",
"native-base": "^3.4.28",
"react": "18.3.1",
"react-dom": "^18.3.1",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { ButtonProps, StyleSheet, Text, TouchableOpacity } from 'react-native';
import { colors } from '../constants';
import Svg, { Circle } from 'react-native-svg';
Expand Down
182 changes: 165 additions & 17 deletions src/components/MagicBellProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,171 @@
import * as MagicBell from '@magicbell/react-headless';
import React, { PropsWithChildren } from 'react';
import React, { createContext, useContext, useState, useCallback, useMemo, ReactNode } from 'react';
import { Client, Notification } from 'magicbell-js/user-client';
import { useCredentials } from '../hooks/useAuth';

interface IProps {}
export default function MagicBellProvider({ children }: PropsWithChildren<IProps>) {
const [credentials] = useCredentials();
type ListNotificationsParams = {
limit?: number;
startingAfter?: string;
endingBefore?: string;
status?: string;
category?: string;
topic?: string;
};

type MagicBellContextType = {
client: Client | null;
notifications: Notification[];
isLoading: boolean;
error: Error | null;
fetchNotifications: (params?: ListNotificationsParams) => Promise<void>;
refreshNotifications: () => Promise<void>;
markAsRead: (notificationId: string) => Promise<void>;
markAsUnread: (notificationId: string) => Promise<void>;
archiveNotification: (notificationId: string) => Promise<void>;
};

const MagicBellContext = createContext<MagicBellContextType | undefined>(undefined);

if (credentials) {
return (
<MagicBell.MagicBellProvider
apiKey={credentials.apiKey}
userEmail={credentials.userEmail}
userKey={credentials.userHmac}
serverURL={credentials.serverURL}
>
<>{children}</>
</MagicBell.MagicBellProvider>
);
export const useMagicBell = () => {
const context = useContext(MagicBellContext);
if (!context) {
throw new Error('useMagicBell must be used within MagicBellProvider');
}
return context;
};

type MagicBellProviderProps = {
children: ReactNode;
};

export default function MagicBellProvider({ children }: MagicBellProviderProps) {
const [credentials] = useCredentials();
const [notifications, setNotifications] = useState<Notification[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);

const client = useMemo(() => {
if (!credentials?.userJWT) {
return null;
}
return new Client({
token: credentials.userJWT,
baseUrl: credentials.serverURL,
});
}, [credentials?.userJWT, credentials?.serverURL]);

const fetchNotifications = useCallback(
async (params?: ListNotificationsParams) => {
if (!client) {
setError(new Error('MagicBell client not initialized'));
return;
}

setIsLoading(true);
setError(null);

try {
const response = await client.notifications.listNotifications({
limit: params?.limit || 50,
...params,
});

setNotifications(response.data?.data || []);
} catch (err) {
const error = err instanceof Error ? err : new Error('Failed to fetch notifications');
setError(error);
console.error('Error fetching notifications:', error);
setNotifications([]);
} finally {
setIsLoading(false);
}
},
[client],
);

const refreshNotifications = useCallback(async () => {
await fetchNotifications();
}, [fetchNotifications]);

const markAsRead = useCallback(
async (notificationId: string) => {
if (!client) return;

try {
await client.notifications.markNotificationRead(notificationId);

setNotifications((prev) =>
prev.map((notification) =>
notification.id === notificationId ? { ...notification, readAt: new Date().toISOString() } : notification,
),
);
} catch (err) {
console.error('Error marking notification as read:', err);
throw err;
}
},
[client],
);

const markAsUnread = useCallback(
async (notificationId: string) => {
if (!client) return;

try {
await client.notifications.markNotificationUnread(notificationId);

setNotifications((prev) =>
prev.map((notification) =>
notification.id === notificationId ? { ...notification, readAt: null } : notification,
),
);
} catch (err) {
console.error('Error marking notification as unread:', err);
throw err;
}
},
[client],
);

const archiveNotification = useCallback(
async (notificationId: string) => {
if (!client) return;

try {
await client.notifications.archiveNotification(notificationId);

setNotifications((prev) => prev.filter((notification) => notification.id !== notificationId));
} catch (err) {
console.error('Error archiving notification:', err);
throw err;
}
},
[client],
);

const value = useMemo(
() => ({
client,
notifications,
isLoading,
error,
fetchNotifications,
refreshNotifications,
markAsRead,
markAsUnread,
archiveNotification,
}),
[
client,
notifications,
isLoading,
error,
fetchNotifications,
refreshNotifications,
markAsRead,
markAsUnread,
archiveNotification,
],
);

return children;
return <MagicBellContext.Provider value={value}>{children}</MagicBellContext.Provider>;
}
8 changes: 4 additions & 4 deletions src/components/Notification.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { IRemoteNotification } from '@magicbell/react-headless';
import { Notification as NotificationType } from 'magicbell-js/user-client';
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { navigationRef } from '../Navigator';
import { CommonActions } from '@react-navigation/native';
import { colors, routes } from '../constants';

interface IProps {
data: IRemoteNotification;
data: NotificationType;
}

const styles = StyleSheet.create({
Expand Down Expand Up @@ -76,8 +76,8 @@ export default function Notification(props: IProps) {
}
};

// convert sentAt timestamp to a human-readable format such as "2 hours ago"
const sentAt = new Date(+props.data.sentAt! * 1000);
// convert createdAt timestamp to a human-readable format such as "2 hours ago"
const sentAt = new Date(props.data.createdAt);
const sentAtString = convertTimestamp(sentAt);

return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ const styles = StyleSheet.create({
});

export default function CustomTextInput(props: TextInputProps) {
return <TextInput {...props} style={styles.input} />;
return <TextInput {...props} autoCapitalize="none" placeholderTextColor="#858585" style={styles.input} />;
}
15 changes: 5 additions & 10 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,17 @@ export const routes = {

export const config: { [key: string]: Credentials } = {
prod: {
apiKey: 'd6a3cf19179a45a5daa9ac7f3f37e9d49914d2ad',
userEmail: 'matt@magicbell.io',
userHmac: '5n4ooUtzydnYq5GYh6PIWGeP2alepTf/Qgb/Sp/g3Co=',
serverURL: 'https://api.magicbell.com',
serverURL: 'https://api.magicbell.com/v2',
userJWT:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2VtYWlsIjpudWxsLCJ1c2VyX2V4dGVybmFsX2lkIjoiN2Y0YmFhYjUtMGM5MS00NGU4LThiNTgtNWZmODQ5NTM1MTc0IiwiYXBpX2tleSI6IjVmNWNmYjI5NTEzODQ2NDMzZTgxYjkxZWM1ZTkwOGM5NDNmZjYwNTgiLCJpYXQiOjE3NjM1NDMyOTksImV4cCI6MTc2MzYyOTY5OX0.NzZcuIv_g-nW0JAhF0i_pH4T96BHCfkdjkJOLnqvF6M',
},
local: {
apiKey: '8cd17191a14339cb1d4e58c4ea471eeca51d2c70',
userEmail: 'matt@magicbell.io',
userHmac: '',
serverURL: 'https://1b35-79-153-3-135.ngrok-free.app',
userJWT: '',
},
review: {
apiKey: '552efd58f59315d065e45b07f8d8f8a2751c2b5b',
userEmail: 'matthewoxley001@gmail.com',
userHmac: '5n4ooUtzydnYq5GYh6PIWGeP2alepTf/Qgb/Sp/g3Co=',
serverURL: 'https://api-4374.magicbell.cloud/',
userJWT: '',
},
};

Expand Down
Loading