Skip to content

API v2 #21

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"preset": "jest-expo"
},
"dependencies": {
"@magicbell/react-headless": "5.1.0",
"@magicbell/react-headless": "5.2.0",
"@magicbell/user-client": "0.7.0",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-navigation/native": "^7.0.14",
"@react-navigation/native-stack": "^7.2.0",
Expand Down
7 changes: 1 addition & 6 deletions src/components/MagicBellProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@ export default function MagicBellProvider({ children }: PropsWithChildren<IProps

if (credentials) {
return (
<MagicBell.MagicBellProvider
apiKey={credentials.apiKey}
userEmail={credentials.userEmail}
userKey={credentials.userHmac}
serverURL={credentials.serverURL}
>
<MagicBell.MagicBellProvider serverURL={credentials.serverURL} token={credentials.userJWTToken}>
<>{children}</>
</MagicBell.MagicBellProvider>
);
Expand Down
16 changes: 3 additions & 13 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,12 @@ export const routes = {

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

Expand Down
31 changes: 13 additions & 18 deletions src/hooks/useAuth.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';

import { UserClient } from 'magicbell/user-client';
import { Client as UserClient } from '@magicbell/user-client';
import useDeviceToken from './useDeviceToken';

const storageKey = 'mb';
const storageKey = 'mbv2';

export type Credentials = {
apiKey: string;
userEmail: string;
userHmac: string;
userJWTToken: string;
serverURL: string;
};

Expand Down Expand Up @@ -40,8 +38,8 @@ export default function CredentialsProvider({ children }: { children: React.Reac
if (validCredentials) {
setCredentials(validCredentials);
} else {
await deleteCredentials();
setCredentials(null);
alert('Invalid # credentials');
}
}, []);
const signOut = useCallback(async () => {
Expand All @@ -64,26 +62,23 @@ const getCredentials = async () => {
return null;
}
try {
const { apiKey, userEmail, userHmac, serverURL } = JSON.parse(value);
const { serverURL, userJWTToken } = JSON.parse(value);
const client = new UserClient({
apiKey: apiKey,
userEmail: userEmail,
userHmac: userHmac,
host: serverURL,
token: userJWTToken,
baseUrl: `${serverURL}/v2`,
});
const config = await client.request({
method: 'GET',
path: '/config',
});
if (config) {
return { apiKey, userEmail, userHmac, serverURL };

// Doing a basic request to see if the token is valid.
// TODO: replace this with a more generic endpoint like `/v2/config` once that's available in the API spec
const testResponse = await client.channels.getMobilePushApnsTokens();
if (testResponse) {
return { serverURL, userJWTToken };
}
} catch (e) {
console.error('Error parsing credentials', e);
await deleteCredentials();
return null;
}
return null;
};

const storeCredentials = async (value: Credentials) => {
Expand Down
86 changes: 40 additions & 46 deletions src/hooks/useDeviceToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,72 +5,66 @@ import {
getIosPushNotificationServiceEnvironmentAsync,
} from 'expo-application';
import { getDevicePushTokenAsync, requestPermissionsAsync } from 'expo-notifications';
import { UserClient } from 'magicbell/user-client';
import { ApnsToken, FcmToken, Client as UserClient } from '@magicbell/user-client';
import React, { useEffect } from 'react';
import { Platform } from 'react-native';
import { Credentials } from './useAuth';

const clientWithCredentials = (credentials: Credentials) =>
new UserClient({
apiKey: credentials.apiKey,
userEmail: credentials.userEmail,
userHmac: credentials.userHmac,
host: credentials.serverURL,
});

const tokenPath = Platform.select({
ios: '/channels/mobile_push/apns/tokens',
android: '/channels/mobile_push/fcm/tokens',
})!;

const apnsTokenPayload = async (token: string): Promise<any> => {
const apnsTokenPayload = async (token: string): Promise<ApnsToken> => {
const isSimulator = (await getIosApplicationReleaseTypeAsync()) === ApplicationReleaseType.SIMULATOR;
const installationId =
(await getIosPushNotificationServiceEnvironmentAsync()) || isSimulator ? 'development' : 'production';
return {
apns: {
device_token: token,
installation_id: installationId,
app_id: applicationId,
},
deviceToken: token,
installationId: installationId,
appId: applicationId || undefined,
};
};

const fcmTokenPayload = (token: string): any => {
const fcmTokenPayload = (token: string): FcmToken => {
return {
fcm: {
device_token: token,
},
deviceToken: token,
};
};

/**
* Registers the device token with the MagicBell API (v2).
*
* Note that the v2 payload differs from the v1 payload.
* Make sure you are using the latest API spec before copying this approach, or check out the previous version in the git history:
* https://github.com/magicbell/mobile-inbox/blob/08448958455cd9beffc6fd4a1469d2c16bc93b22/src/hooks/useDeviceToken.tsx#L21-L61
*/
const registerTokenWithCredentials = async (token: string, credentials: Credentials) => {
const data = Platform.OS === 'ios' ? await apnsTokenPayload(token) : fcmTokenPayload(token);

console.log('posting token', token);
const client = clientWithCredentials(credentials);
client
.request({
method: 'POST',
path: tokenPath,
data: data,
})
.catch((err) => {
console.log('post token error', err);
});
const client = new UserClient({ baseUrl: `${credentials.serverURL}/v2`, token: credentials.userJWTToken });
console.info('posting token', token);
if (Platform.OS === 'ios') {
const payload = await apnsTokenPayload(token);
try {
await client.channels.saveMobilePushApnsToken(payload);
} catch (error) {
console.error('Error registering APNS token: ', error);
}
} else if (Platform.OS === 'android') {
const payload = await fcmTokenPayload(token);
try {
await client.channels.saveMobilePushFcmToken(payload);
} catch (error) {
console.error('Error registering FCM token: ', error);
}
} else {
console.warn(`not posting token on platform ${Platform.OS}`);
}
};

const unregisterTokenWithCredentials = async (token: string, credentials: Credentials) => {
console.log('deleting token', token);
const client = clientWithCredentials(credentials);
client
.request({
method: 'DELETE',
path: tokenPath + '/' + token,
})
.catch((err) => {
console.log('delete token error', err);
});
const client = new UserClient({ baseUrl: `${credentials.serverURL}/v2`, token: credentials.userJWTToken });

if (Platform.OS === 'ios') {
await client.channels.discardMobilePushApnsToken(token);
} else if (Platform.OS === 'android') {
await client.channels.discardMobilePushFcmToken(token);
}
};

export default function useDeviceToken(credentials: Credentials | null | undefined) {
Expand Down
15 changes: 4 additions & 11 deletions src/hooks/useReviewCredentials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,14 @@ import { Platform } from 'react-native';
* ATTENTION: This is only for MagicBell internal use. You should not follow this example in your production app.
*
* Example URL:
* x-magicbell-review://connect?apiHost=[...]&apiKey=[...]&userEmail=[...]&userHmac=[...]
* x-magicbell-review://connect?apiHost=[...]&userJWTToken=[...]
*
*/
const parseLaunchURLCredentials = (url: URL): Credentials | null => {
var serverURL = url.searchParams.get('apiHost');
const apiKey = url.searchParams.get('apiKey');
const userJWTToken = url.searchParams.get('userJWTToken');

// TODO: support userExternalID as well
const userEmail = url.searchParams.get('userEmail');

const userHmac = url.searchParams.get('userHmac');

if (!serverURL || !apiKey || !userEmail || !userHmac) {
if (!serverURL || !userJWTToken) {
console.warn('Could not parse credentials from launch URL: ', url.toString());
return null;
}
Expand All @@ -37,9 +32,7 @@ const parseLaunchURLCredentials = (url: URL): Credentials | null => {

const credentials: Credentials = {
serverURL,
apiKey,
userHmac,
userEmail,
userJWTToken,
};
return credentials;
};
Expand Down
20 changes: 6 additions & 14 deletions src/screens/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,20 @@ export const SignInScreen = (): React.JSX.Element => {
const defaultCredentials = reviewCredentials || currentConfig;
const [loading, setLoading] = useState(false);
const [serverURL, setServerURL] = useState(defaultCredentials.serverURL);
const [apiKey, setApiKey] = useState(defaultCredentials.apiKey);
const [userEmail, setUserEmail] = useState(defaultCredentials.userEmail);
const [userHmac, setUserHmac] = useState(defaultCredentials.userHmac);
const [userJWTToken, setUserJWTToken] = useState(defaultCredentials.userJWTToken);

useEffect(() => {
if (reviewCredentials) {
setServerURL(reviewCredentials.serverURL);
setApiKey(reviewCredentials.apiKey);
setUserEmail(reviewCredentials.userEmail);
setUserHmac(reviewCredentials.userHmac);
setUserJWTToken(reviewCredentials.userJWTToken);
}
}, [reviewCredentials]);

const handleSubmit = useCallback(async () => {
setLoading(true);
await signIn({ apiKey, userEmail, userHmac, serverURL });
await signIn({ serverURL, userJWTToken });
setLoading(false);
}, [signIn, apiKey, userEmail, userHmac, serverURL]);
}, [signIn, serverURL, userJWTToken]);

if (credentials) {
throw new Error('User is already signed in');
Expand Down Expand Up @@ -75,9 +71,7 @@ export const SignInScreen = (): React.JSX.Element => {
onValueChange={
((itemValue: keyof typeof config) => {
const c = config[itemValue];
setApiKey(c.apiKey);
setUserEmail(c.userEmail);
setUserHmac(c.userHmac);
setUserJWTToken(c.userJWTToken);
setServerURL(c.serverURL);
}) as (itemValue: string) => void
}
Expand All @@ -87,9 +81,7 @@ export const SignInScreen = (): React.JSX.Element => {
})}
</Select>
</Box>
<TextInput placeholder="Project API Key" value={apiKey} onChangeText={setApiKey} />
<TextInput placeholder="User email" value={userEmail} onChangeText={setUserEmail} />
<TextInput placeholder="User HMAC" value={userHmac} onChangeText={setUserHmac} />
<TextInput placeholder="User JWT Token" value={userJWTToken} onChangeText={setUserJWTToken} />
<TextInput placeholder="Server URL" value={serverURL} onChangeText={setServerURL} />
<CustomButton title="#" loading={loading} onPress={handleSubmit} />
</View>
Expand Down
Loading