From af2a0bec58cefbb8c8cde7754f70c0e298e82d77 Mon Sep 17 00:00:00 2001 From: Niya Gupta Date: Mon, 17 Nov 2025 13:59:01 +0530 Subject: [PATCH 1/8] feat: add expo-dev-client for dev builds --- README.md | 4 +- package.json | 1 + yarn.lock | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 149 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c53f4fa..3373878 100644 --- a/README.md +++ b/README.md @@ -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). @@ -33,6 +33,8 @@ You will also need [NodeJS](https://nodejs.org) and [Yarn](https://yarnpkg.com) More details on how to set up a React Native dev environment can be found on [reactnative.dev](https://reactnative.dev/docs/environment-setup). +Install the dependencies by running `yarn`. + ## Starting A Local Development Build You can simply get started by running the main dev command, `yarn start`. This will start Metro Bundler and bring up the Expo dev dashboard. diff --git a/package.json b/package.json index 731e2d4..3a28d35 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "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", diff --git a/yarn.lock b/yarn.lock index e10022e..9258c24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1228,6 +1228,26 @@ xcode "^3.0.1" xml2js "0.6.0" +"@expo/config-plugins@~9.0.17": + version "9.0.17" + resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-9.0.17.tgz#c997072209129b9f9616efa3533314b889cfd788" + integrity sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg== + dependencies: + "@expo/config-types" "^52.0.5" + "@expo/json-file" "~9.0.2" + "@expo/plist" "^0.2.2" + "@expo/sdk-runtime-versions" "^1.0.0" + chalk "^4.1.2" + debug "^4.3.5" + getenv "^1.0.0" + glob "^10.4.2" + resolve-from "^5.0.0" + semver "^7.5.4" + slash "^3.0.0" + slugify "^1.6.6" + xcode "^3.0.1" + xml2js "0.6.0" + "@expo/config-types@^47.0.0": version "47.0.0" resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-47.0.0.tgz#99eeabe0bba7a776e0f252b78beb0c574692c38d" @@ -1238,6 +1258,30 @@ resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-52.0.3.tgz#511f2f868172c93abeac7183beeb921dc72d6e1e" integrity sha512-muxvuARmbysH5OGaiBRlh1Y6vfdmL56JtpXxB+y2Hfhu0ezG1U4FjZYBIacthckZPvnDCcP3xIu1R+eTo7/QFA== +"@expo/config-types@^52.0.5": + version "52.0.5" + resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-52.0.5.tgz#e10a226990dd903a4e3db5992ffb3015adf13f38" + integrity sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA== + +"@expo/config@~10.0.11": + version "10.0.11" + resolved "https://registry.yarnpkg.com/@expo/config/-/config-10.0.11.tgz#5371ccb3b08ece4c174d5d7009d61e928e6925b0" + integrity sha512-nociJ4zr/NmbVfMNe9j/+zRlt7wz/siISu7PjdWE4WE+elEGxWWxsGzltdJG0llzrM+khx8qUiFK5aiVcdMBww== + dependencies: + "@babel/code-frame" "~7.10.4" + "@expo/config-plugins" "~9.0.17" + "@expo/config-types" "^52.0.5" + "@expo/json-file" "^9.0.2" + deepmerge "^4.3.1" + getenv "^1.0.0" + glob "^10.4.2" + require-from-string "^2.0.2" + resolve-from "^5.0.0" + resolve-workspace-root "^2.0.0" + semver "^7.6.0" + slugify "^1.3.4" + sucrase "3.35.0" + "@expo/config@~10.0.8": version "10.0.8" resolved "https://registry.yarnpkg.com/@expo/config/-/config-10.0.8.tgz#c94cf98328d2ec38c9da80ec68d252539cd6eb2d" @@ -1384,6 +1428,23 @@ json5 "^2.2.3" write-file-atomic "^2.3.0" +"@expo/json-file@^9.0.2": + version "9.1.5" + resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-9.1.5.tgz#7d7b2dc4990dc2c2de69a571191aba984b7fb7ed" + integrity sha512-prWBhLUlmcQtvN6Y7BpW2k9zXGd3ySa3R6rAguMJkp1z22nunLN64KYTUWfijFlprFoxm9r2VNnGkcbndAlgKA== + dependencies: + "@babel/code-frame" "~7.10.4" + json5 "^2.2.3" + +"@expo/json-file@~9.0.2": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-9.0.2.tgz#ec508c2ad17490e0c664c9d7e2ae0ce65915d3ed" + integrity sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw== + dependencies: + "@babel/code-frame" "~7.10.4" + json5 "^2.2.3" + write-file-atomic "^2.3.0" + "@expo/metro-config@0.19.9", "@expo/metro-config@~0.19.9": version "0.19.9" resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.19.9.tgz#f020a2523cecf90e4f2a833386a88e07f6d004f8" @@ -1452,6 +1513,15 @@ base64-js "^1.2.3" xmlbuilder "^14.0.0" +"@expo/plist@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.2.2.tgz#2563b71b4aa78dc9dbc34cc3d2e1011e994bc9cd" + integrity sha512-ZZGvTO6vEWq02UAPs3LIdja+HRO18+LRI5QuDl6Hs3Ps7KX7xU6Y6kjahWKY37Rx2YjNpX07dGpBFzzC+vKa2g== + dependencies: + "@xmldom/xmldom" "~0.7.7" + base64-js "^1.2.3" + xmlbuilder "^14.0.0" + "@expo/prebuild-config@5.0.7": version "5.0.7" resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-5.0.7.tgz#4658b66126c4d32c7b6302571e458a71811b07aa" @@ -3062,6 +3132,16 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ajv@8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + anser@^1.4.9: version "1.4.10" resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b" @@ -4373,6 +4453,39 @@ expo-constants@~17.0.4: "@expo/config" "~10.0.8" "@expo/env" "~0.4.1" +expo-dev-client@~5.0.20: + version "5.0.20" + resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-5.0.20.tgz#349a6251d1d63c3142ad5232be653038b5c6cf15" + integrity sha512-bLNkHdU7V3I4UefgJbJnIDUBUL0LxIal/xYEx9BbgDd3B7wgQKY//+BpPIxBOKCQ22lkyiHY8y9tLhO903sAgg== + dependencies: + expo-dev-launcher "5.0.35" + expo-dev-menu "6.0.25" + expo-dev-menu-interface "1.9.3" + expo-manifests "~0.15.8" + expo-updates-interface "~1.0.0" + +expo-dev-launcher@5.0.35: + version "5.0.35" + resolved "https://registry.yarnpkg.com/expo-dev-launcher/-/expo-dev-launcher-5.0.35.tgz#098004658ccb9a55f4170427eb1a35eaf42cea17" + integrity sha512-hEQr0ZREnUMxZ6wtQgfK1lzYnbb0zar3HqYZhmANzXmE6UEPbQ4GByLzhpfz/d+xxdBVQZsrHdtiV28KPG2sog== + dependencies: + ajv "8.11.0" + expo-dev-menu "6.0.25" + expo-manifests "~0.15.8" + resolve-from "^5.0.0" + +expo-dev-menu-interface@1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/expo-dev-menu-interface/-/expo-dev-menu-interface-1.9.3.tgz#5dc618e498b286a50a9272a8bc71969b6db54e23" + integrity sha512-KY/dWTBE1l47i9V366JN5rC6YIdOc9hz8yAmZzkl5DrPia5l3M2WIjtnpHC9zUkNjiSiG2urYoOAq4H/uLdmyg== + +expo-dev-menu@6.0.25: + version "6.0.25" + resolved "https://registry.yarnpkg.com/expo-dev-menu/-/expo-dev-menu-6.0.25.tgz#72b4607b33d0d6a3823561b1dfe1759a02a86e4a" + integrity sha512-K2m4z/I+CPWbMtHlDzU68lHaQs52De0v5gbsjAmA5ig8FrYh4MKZvPxSVANaiKENzgmtglu8qaFh7ua9Gt2TfA== + dependencies: + expo-dev-menu-interface "1.9.3" + expo-file-system@~18.0.7: version "18.0.7" resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-18.0.7.tgz#218792dc0aeb7e0976a7f8f412a5d7de09b39610" @@ -4387,6 +4500,11 @@ expo-font@~13.0.3: dependencies: fontfaceobserver "^2.1.0" +expo-json-utils@~0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/expo-json-utils/-/expo-json-utils-0.14.0.tgz#ad3cbbcb4fb22e4d23bf9fb19b611e36758861d2" + integrity sha512-xjGfK9dL0B1wLnOqNkX0jM9p48Y0I5xEPzHude28LY67UmamUyAACkqhZGaPClyPNfdzczk7Ej6WaRMT3HfXvw== + expo-keep-awake@~14.0.2: version "14.0.2" resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-14.0.2.tgz#124a729df43c87994631f51d5b1b5093d58e6c80" @@ -4400,6 +4518,14 @@ expo-linking@~7.0.4: expo-constants "~17.0.4" invariant "^2.2.4" +expo-manifests@~0.15.8: + version "0.15.8" + resolved "https://registry.yarnpkg.com/expo-manifests/-/expo-manifests-0.15.8.tgz#15e7b7b99d764b40ca3e3f859a126c856e2d6206" + integrity sha512-VuIyaMfRfLZeETNsRohqhy1l7iZ7I+HKMPfZXVL2Yn17TT0WkOhZoq1DzYwPbOHPgp1Uk6phNa86EyaHrD2DLw== + dependencies: + "@expo/config" "~10.0.11" + expo-json-utils "~0.14.0" + expo-modules-autolinking@2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-2.0.7.tgz#fc40ba7505f42f971253ea20a927693f2c123a56" @@ -4449,6 +4575,11 @@ expo-splash-screen@~0.29.21: dependencies: "@expo/prebuild-config" "^8.0.25" +expo-updates-interface@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-1.0.0.tgz#b98c66b800d29561c62409556948b2af3d5316e5" + integrity sha512-93oWtvULJOj+Pp+N/lpTcFfuREX1wNeHtp7Lwn8EbzYYmdn37MvZU3TPW2tYYCZuhzmKEXnUblYcruYoDu7IrQ== + expo@^52.0.27: version "52.0.27" resolved "https://registry.yarnpkg.com/expo/-/expo-52.0.27.tgz#9eeceda4990ee5a78a66d3f2c26122118ba9454c" @@ -4483,7 +4614,7 @@ fast-base64-decode@^1.0.0: resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418" integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== -fast-deep-equal@^3.1.3: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== @@ -5778,6 +5909,11 @@ json-schema-to-ts@3.1.0: "@babel/runtime" "^7.18.3" ts-algebra "^2.0.0" +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-stable-stringify@^1.0.2: version "1.2.1" resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz#addb683c2b78014d0b78d704c2fcbdf0695a60e2" @@ -7058,7 +7194,7 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^2.1.1: +punycode@^2.1.0, punycode@^2.1.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== @@ -8262,6 +8398,13 @@ update-browserslist-db@^1.1.1: escalade "^3.2.0" picocolors "^1.1.1" +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + url-join@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" From 7e1d4d927ea7687c62558b5f18957e642bc596dd Mon Sep 17 00:00:00 2001 From: Niya Gupta Date: Mon, 17 Nov 2025 15:46:10 +0530 Subject: [PATCH 2/8] refactor: use jwt secret for v2 auth --- package.json | 2 ++ src/components/TextInput.tsx | 2 +- src/constants.ts | 6 ++-- src/hooks/useAuth.tsx | 67 ++++++++++++++++++++++++++++-------- src/screens/SignIn.tsx | 12 +++---- yarn.lock | 21 ++++++++++- 6 files changed, 85 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 3a28d35..4f18f03 100644 --- a/package.json +++ b/package.json @@ -25,12 +25,14 @@ "expo": "^52.0.27", "expo-app-loading": "^2.1.1", "expo-application": "~6.0.2", + "expo-crypto": "~14.0.1", "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", diff --git a/src/components/TextInput.tsx b/src/components/TextInput.tsx index 99e72df..eaf2780 100644 --- a/src/components/TextInput.tsx +++ b/src/components/TextInput.tsx @@ -17,5 +17,5 @@ const styles = StyleSheet.create({ }); export default function CustomTextInput(props: TextInputProps) { - return ; + return ; } diff --git a/src/constants.ts b/src/constants.ts index 910c2f1..9932238 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -76,20 +76,20 @@ export const routes = { export const config: { [key: string]: Credentials } = { prod: { apiKey: 'd6a3cf19179a45a5daa9ac7f3f37e9d49914d2ad', + secretKey: '', userEmail: 'matt@magicbell.io', - userHmac: '5n4ooUtzydnYq5GYh6PIWGeP2alepTf/Qgb/Sp/g3Co=', serverURL: 'https://api.magicbell.com', }, local: { apiKey: '8cd17191a14339cb1d4e58c4ea471eeca51d2c70', + secretKey: '', userEmail: 'matt@magicbell.io', - userHmac: '', serverURL: 'https://1b35-79-153-3-135.ngrok-free.app', }, review: { apiKey: '552efd58f59315d065e45b07f8d8f8a2751c2b5b', + secretKey: '', userEmail: 'matthewoxley001@gmail.com', - userHmac: '5n4ooUtzydnYq5GYh6PIWGeP2alepTf/Qgb/Sp/g3Co=', serverURL: 'https://api-4374.magicbell.cloud/', }, }; diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 927f528..738d895 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -1,15 +1,16 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; +import * as Crypto from 'expo-crypto'; -import { UserClient } from 'magicbell/user-client'; +import { Client } from 'magicbell-js/project-client'; import useDeviceToken from './useDeviceToken'; const storageKey = 'mb'; export type Credentials = { apiKey: string; + secretKey: string; userEmail: string; - userHmac: string; serverURL: string; }; @@ -64,19 +65,22 @@ const getCredentials = async () => { return null; } try { - const { apiKey, userEmail, userHmac, serverURL } = JSON.parse(value); - const client = new UserClient({ - apiKey: apiKey, - userEmail: userEmail, - userHmac: userHmac, - host: serverURL, - }); - const config = await client.request({ - method: 'GET', - path: '/config', + const { apiKey, userEmail, secretKey, serverURL } = JSON.parse(value); + + const payload = { + user_email: userEmail, + user_external_id: null, + api_key: apiKey, + }; + + const token = await createJWT(payload, secretKey); + + const client = new Client({ + token, }); - if (config) { - return { apiKey, userEmail, userHmac, serverURL }; + + if (client.config) { + return { apiKey, userEmail, secretKey, serverURL }; } } catch (e) { console.error('Error parsing credentials', e); @@ -94,3 +98,38 @@ const storeCredentials = async (value: Credentials) => { const deleteCredentials = async () => { await AsyncStorage.removeItem(storageKey); }; + +// Helper function to convert string to base64url encoding +const base64UrlEncode = (str: string): string => { + const base64 = btoa(str); + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +}; + +// Create JWT using expo-crypto for HMAC signing +const createJWT = async (payload: object, secret: string): Promise => { + const header = { + alg: 'HS256', + typ: 'JWT', + }; + + const now = Math.floor(Date.now() / 1000); + const tokenPayload = { + ...payload, + iat: now, + exp: now + 86400, // 1 day + }; + + const encodedHeader = base64UrlEncode(JSON.stringify(header)); + const encodedPayload = base64UrlEncode(JSON.stringify(tokenPayload)); + const message = `${encodedHeader}.${encodedPayload}`; + + // Create HMAC signature using expo-crypto + const signature = await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, secret + message); + + // Convert hex signature to base64url + const signatureBytes = signature.match(/.{2}/g)?.map((byte: string) => parseInt(byte, 16)) || []; + const signatureStr = String.fromCharCode(...signatureBytes); + const encodedSignature = base64UrlEncode(signatureStr); + + return `${message}.${encodedSignature}`; +}; diff --git a/src/screens/SignIn.tsx b/src/screens/SignIn.tsx index 7c6f2e0..9b0ee3d 100644 --- a/src/screens/SignIn.tsx +++ b/src/screens/SignIn.tsx @@ -18,22 +18,22 @@ export const SignInScreen = (): React.JSX.Element => { 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 [secretKey, setSecretKey] = useState(defaultCredentials.secretKey); useEffect(() => { if (reviewCredentials) { setServerURL(reviewCredentials.serverURL); setApiKey(reviewCredentials.apiKey); setUserEmail(reviewCredentials.userEmail); - setUserHmac(reviewCredentials.userHmac); + setSecretKey(reviewCredentials.secretKey); } }, [reviewCredentials]); const handleSubmit = useCallback(async () => { setLoading(true); - await signIn({ apiKey, userEmail, userHmac, serverURL }); + await signIn({ apiKey, userEmail, secretKey, serverURL }); setLoading(false); - }, [signIn, apiKey, userEmail, userHmac, serverURL]); + }, [signIn, apiKey, userEmail, secretKey, serverURL]); if (credentials) { throw new Error('User is already signed in'); @@ -77,7 +77,7 @@ export const SignInScreen = (): React.JSX.Element => { const c = config[itemValue]; setApiKey(c.apiKey); setUserEmail(c.userEmail); - setUserHmac(c.userHmac); + setSecretKey(c.secretKey); setServerURL(c.serverURL); }) as (itemValue: string) => void } @@ -88,8 +88,8 @@ export const SignInScreen = (): React.JSX.Element => { + - diff --git a/yarn.lock b/yarn.lock index 9258c24..7960124 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3421,7 +3421,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.2.3, base64-js@^1.3.1, base64-js@^1.5.1: +base64-js@^1.2.3, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -4453,6 +4453,13 @@ expo-constants@~17.0.4: "@expo/config" "~10.0.8" "@expo/env" "~0.4.1" +expo-crypto@~14.0.1: + version "14.0.2" + resolved "https://registry.yarnpkg.com/expo-crypto/-/expo-crypto-14.0.2.tgz#5f5d83c849164229f7a3e6a341887142756d517e" + integrity sha512-WRc9PBpJraJN29VD5Ef7nCecxJmZNyRKcGkNiDQC1nhY5agppzwhqh7zEzNFarE/GqDgSiaDHS8yd5EgFhP9AQ== + dependencies: + base64-js "^1.3.0" + expo-dev-client@~5.0.20: version "5.0.20" resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-5.0.20.tgz#349a6251d1d63c3142ad5232be653038b5c6cf15" @@ -6189,6 +6196,13 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +magicbell-js@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/magicbell-js/-/magicbell-js-1.4.0.tgz#8a060a6f904d1fd0469689a60b51c11bdc9ff613" + integrity sha512-8eJes4+HRfSMr8IrkBV02DMP9TDMdKHeUms2g5z6mz2jS06fO+P+sxSjX4Cy5eJh3lvWjjMmT5mHRiKxuvxTyQ== + dependencies: + zod "3.22.0" + magicbell@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/magicbell/-/magicbell-4.1.0.tgz#3a713fa3f2ff2663d56081c9b3c88175c39c0078" @@ -8726,6 +8740,11 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zod@3.22.0: + version "3.22.0" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.0.tgz#2478211a9bf477eb2d7d2ce031b5f8ff0d596407" + integrity sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q== + zustand@^4.5.2: version "4.5.5" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.5.tgz#f8c713041543715ec81a2adda0610e1dc82d4ad1" From 78351020ef5d572d980372c3f952be960fbebb5c Mon Sep 17 00:00:00 2001 From: Niya Gupta Date: Tue, 18 Nov 2025 14:24:18 +0530 Subject: [PATCH 3/8] refactor: migrate libraries to v2 --- index.js | 4 +- ios/ci_scripts/ci_post_clone.sh | 13 -- package.json | 1 - src/components/MagicBellProvider.tsx | 212 +++++++++++++++++++++-- src/components/Notification.tsx | 8 +- src/constants.ts | 12 +- src/hooks/useAuth.tsx | 59 +------ src/hooks/useDeviceToken.tsx | 57 +++--- src/hooks/usePushNotificationHandler.tsx | 2 - src/hooks/useReviewCredentials.tsx | 14 +- src/screens/Details.tsx | 6 +- src/screens/Home.tsx | 16 +- src/screens/SignIn.tsx | 19 +- yarn.lock | 9 +- 14 files changed, 259 insertions(+), 173 deletions(-) delete mode 100755 ios/ci_scripts/ci_post_clone.sh diff --git a/index.js b/index.js index ffe0f07..4b31f9e 100644 --- a/index.js +++ b/index.js @@ -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); diff --git a/ios/ci_scripts/ci_post_clone.sh b/ios/ci_scripts/ci_post_clone.sh deleted file mode 100755 index 376f080..0000000 --- a/ios/ci_scripts/ci_post_clone.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -set -e -echo "Running ci_post_clone.sh" - -# cd out of ios/ci_scripts into main project directory -cd ../../ - -# install node and cocoapods -HOMEBREW_NO_AUTO_UPDATE=1 brew install node cocoapods -npm install - -npx expo prebuild --platform=ios \ No newline at end of file diff --git a/package.json b/package.json index 4f18f03..b963c55 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "expo": "^52.0.27", "expo-app-loading": "^2.1.1", "expo-application": "~6.0.2", - "expo-crypto": "~14.0.1", "expo-dev-client": "~5.0.20", "expo-font": "~13.0.3", "expo-linking": "~7.0.4", diff --git a/src/components/MagicBellProvider.tsx b/src/components/MagicBellProvider.tsx index 0123695..9cf79e1 100644 --- a/src/components/MagicBellProvider.tsx +++ b/src/components/MagicBellProvider.tsx @@ -1,23 +1,201 @@ -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) { - const [credentials] = useCredentials(); +type MagicBellContextType = { + client: Client | null; + notifications: Notification[]; + isLoading: boolean; + error: Error | null; + fetchNotifications: (params?: { + limit?: number; + startingAfter?: string; + endingBefore?: string; + status?: string; + category?: string; + topic?: string; + }) => Promise; + refreshNotifications: () => Promise; + markAsRead: (notificationId: string) => Promise; + markAsUnread: (notificationId: string) => Promise; + archiveNotification: (notificationId: string) => Promise; +}; + +const MagicBellContext = createContext( + undefined +); - if (credentials) { - return ( - - <>{children} - - ); +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([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Create client with JWT token + const client = useMemo(() => { + if (!credentials?.userJWT) { + return null; + } + return new Client({ + token: credentials.userJWT, + }); + }, [credentials?.userJWT]); + + const fetchNotifications = useCallback( + async (params?: { + limit?: number; + startingAfter?: string; + endingBefore?: string; + status?: string; + category?: string; + topic?: string; + }) => { + 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, + }); + + // The SDK returns HttpResponse + // NotificationCollection has an optional data field + 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); + // Ensure notifications is set to empty array on 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); + + // Optimistically update local state + 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); + + // Optimistically update local state + 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); + + // Optimistically update local state + 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 ( + + {children} + + ); } diff --git a/src/components/Notification.tsx b/src/components/Notification.tsx index c578c82..992fb84 100644 --- a/src/components/Notification.tsx +++ b/src/components/Notification.tsx @@ -1,4 +1,4 @@ -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'; @@ -6,7 +6,7 @@ import { CommonActions } from '@react-navigation/native'; import { colors, routes } from '../constants'; interface IProps { - data: IRemoteNotification; + data: NotificationType; } const styles = StyleSheet.create({ @@ -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 ( diff --git a/src/constants.ts b/src/constants.ts index 9932238..e20d6ad 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -75,22 +75,16 @@ export const routes = { export const config: { [key: string]: Credentials } = { prod: { - apiKey: 'd6a3cf19179a45a5daa9ac7f3f37e9d49914d2ad', - secretKey: '', - userEmail: 'matt@magicbell.io', serverURL: 'https://api.magicbell.com', + userJWT: '', }, local: { - apiKey: '8cd17191a14339cb1d4e58c4ea471eeca51d2c70', - secretKey: '', - userEmail: 'matt@magicbell.io', serverURL: 'https://1b35-79-153-3-135.ngrok-free.app', + userJWT: '', }, review: { - apiKey: '552efd58f59315d065e45b07f8d8f8a2751c2b5b', - secretKey: '', - userEmail: 'matthewoxley001@gmail.com', serverURL: 'https://api-4374.magicbell.cloud/', + userJWT: '', }, }; diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 738d895..a8a7115 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -1,17 +1,15 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; -import * as Crypto from 'expo-crypto'; -import { Client } from 'magicbell-js/project-client'; +import { Client } from 'magicbell-js/user-client'; import useDeviceToken from './useDeviceToken'; const storageKey = 'mb'; +// TODO: refactor to remove other Credientials fields except userJWT and serverURL export type Credentials = { - apiKey: string; - secretKey: string; - userEmail: string; serverURL: string; + userJWT: string; }; type CredentialsContextType = { @@ -65,22 +63,16 @@ const getCredentials = async () => { return null; } try { - const { apiKey, userEmail, secretKey, serverURL } = JSON.parse(value); - - const payload = { - user_email: userEmail, - user_external_id: null, - api_key: apiKey, - }; - - const token = await createJWT(payload, secretKey); + const { apiKey, secretKey, userEmail, serverURL, userJWT } = JSON.parse(value); const client = new Client({ - token, + token: userJWT, }); + // TODO: Verify bad credentials cannot be used + // Use the client to check the credentials are valid if (client.config) { - return { apiKey, userEmail, secretKey, serverURL }; + return { apiKey, userEmail, secretKey, serverURL, userJWT }; } } catch (e) { console.error('Error parsing credentials', e); @@ -98,38 +90,3 @@ const storeCredentials = async (value: Credentials) => { const deleteCredentials = async () => { await AsyncStorage.removeItem(storageKey); }; - -// Helper function to convert string to base64url encoding -const base64UrlEncode = (str: string): string => { - const base64 = btoa(str); - return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); -}; - -// Create JWT using expo-crypto for HMAC signing -const createJWT = async (payload: object, secret: string): Promise => { - const header = { - alg: 'HS256', - typ: 'JWT', - }; - - const now = Math.floor(Date.now() / 1000); - const tokenPayload = { - ...payload, - iat: now, - exp: now + 86400, // 1 day - }; - - const encodedHeader = base64UrlEncode(JSON.stringify(header)); - const encodedPayload = base64UrlEncode(JSON.stringify(tokenPayload)); - const message = `${encodedHeader}.${encodedPayload}`; - - // Create HMAC signature using expo-crypto - const signature = await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, secret + message); - - // Convert hex signature to base64url - const signatureBytes = signature.match(/.{2}/g)?.map((byte: string) => parseInt(byte, 16)) || []; - const signatureStr = String.fromCharCode(...signatureBytes); - const encodedSignature = base64UrlEncode(signatureStr); - - return `${message}.${encodedSignature}`; -}; diff --git a/src/hooks/useDeviceToken.tsx b/src/hooks/useDeviceToken.tsx index 50f6d9a..79e00ff 100644 --- a/src/hooks/useDeviceToken.tsx +++ b/src/hooks/useDeviceToken.tsx @@ -5,33 +5,27 @@ import { getIosPushNotificationServiceEnvironmentAsync, } from 'expo-application'; import { getDevicePushTokenAsync, requestPermissionsAsync } from 'expo-notifications'; -import { UserClient } from 'magicbell/user-client'; +import { ApnsTokenPayload, ApnsTokenPayloadInstallationId, Client } from 'magicbell-js/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, + new Client({ + token: credentials.userJWT, }); -const tokenPath = Platform.select({ - ios: '/channels/mobile_push/apns/tokens', - android: '/channels/mobile_push/fcm/tokens', -})!; - const apnsTokenPayload = async (token: string): Promise => { const isSimulator = (await getIosApplicationReleaseTypeAsync()) === ApplicationReleaseType.SIMULATOR; const installationId = - (await getIosPushNotificationServiceEnvironmentAsync()) || isSimulator ? 'development' : 'production'; + (await getIosPushNotificationServiceEnvironmentAsync()) || isSimulator + ? ApnsTokenPayloadInstallationId.DEVELOPMENT + : ApnsTokenPayloadInstallationId.PRODUCTION; return { apns: { - device_token: token, - installation_id: installationId, - app_id: applicationId, + deviceToken: token, + installationId, + appId: applicationId, }, }; }; @@ -39,7 +33,7 @@ const apnsTokenPayload = async (token: string): Promise => { const fcmTokenPayload = (token: string): any => { return { fcm: { - device_token: token, + deviceToken: token, }, }; }; @@ -49,28 +43,25 @@ const registerTokenWithCredentials = async (token: string, credentials: Credenti 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); - }); + + switch (Platform.OS) { + case 'ios': + client.channels.saveApnsToken(data); + case 'android': + client.channels.saveFcmToken(data); + } }; 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); - }); + + switch (Platform.OS) { + case 'ios': + client.channels.deleteApnsToken(token); + case 'android': + client.channels.deleteFcmToken(token); + } }; export default function useDeviceToken(credentials: Credentials | null | undefined) { diff --git a/src/hooks/usePushNotificationHandler.tsx b/src/hooks/usePushNotificationHandler.tsx index 58a4b07..20fae06 100644 --- a/src/hooks/usePushNotificationHandler.tsx +++ b/src/hooks/usePushNotificationHandler.tsx @@ -1,5 +1,3 @@ -import PushNotificationIOS, { PushNotification } from '@react-native-community/push-notification-ios'; - import { navigationRef } from '../Navigator'; import { useEffect } from 'react'; import { CommonActions } from '@react-navigation/native'; diff --git a/src/hooks/useReviewCredentials.tsx b/src/hooks/useReviewCredentials.tsx index e65d448..a9c0cf3 100644 --- a/src/hooks/useReviewCredentials.tsx +++ b/src/hooks/useReviewCredentials.tsx @@ -10,19 +10,15 @@ 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=[...]&apiKey=[...]&secretKey=[...]&userEmail=[...]&userJWT=[...] * */ const parseLaunchURLCredentials = (url: URL): Credentials | null => { var serverURL = url.searchParams.get('apiHost'); - const apiKey = url.searchParams.get('apiKey'); - // TODO: support userExternalID as well - const userEmail = url.searchParams.get('userEmail'); + const userJWT = url.searchParams.get('userJWT'); - const userHmac = url.searchParams.get('userHmac'); - - if (!serverURL || !apiKey || !userEmail || !userHmac) { + if (!serverURL || !userJWT) { console.warn('Could not parse credentials from launch URL: ', url.toString()); return null; } @@ -37,9 +33,7 @@ const parseLaunchURLCredentials = (url: URL): Credentials | null => { const credentials: Credentials = { serverURL, - apiKey, - userHmac, - userEmail, + userJWT, }; return credentials; }; diff --git a/src/screens/Details.tsx b/src/screens/Details.tsx index 33917be..1b2f555 100644 --- a/src/screens/Details.tsx +++ b/src/screens/Details.tsx @@ -3,7 +3,7 @@ import { StyleSheet, Text, View } from 'react-native'; import { colors, styles } from '../constants'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { convertTimestamp } from '../components/Notification'; -import { IRemoteNotification } from '@magicbell/react-headless'; +import { Notification } from 'magicbell-js/user-client'; const s = StyleSheet.create({ sectionContainer: { @@ -43,8 +43,8 @@ const s = StyleSheet.create({ export default function Details(props: NativeStackScreenProps) { console.log('props', props.route.params); - const params = props.route.params as IRemoteNotification; - const sentAtString = convertTimestamp(new Date(params!.sentAt * 1000)); + const params = props.route.params as Notification; + const sentAtString = convertTimestamp(new Date(params!.createdAt)); return ( diff --git a/src/screens/Home.tsx b/src/screens/Home.tsx index bf072d3..d42cc0a 100644 --- a/src/screens/Home.tsx +++ b/src/screens/Home.tsx @@ -1,22 +1,28 @@ -import React from 'react'; +import React, { useEffect } from 'react'; -import { Button, SafeAreaView, ScrollView } from 'react-native'; +import { Button, SafeAreaView, ScrollView, ActivityIndicator, Text } from 'react-native'; import { styles } from '../constants'; import { useCredentials } from '../hooks/useAuth'; -import { useNotifications } from '@magicbell/react-headless'; +import { useMagicBell } from '../components/MagicBellProvider'; import Notification from '../components/Notification'; import usePushNotificationHandler from '../hooks/usePushNotificationHandler'; export default function HomeScreen(): React.JSX.Element { const [_, __, logout] = useCredentials(); - const store = useNotifications(); + const { notifications, isLoading, error, fetchNotifications } = useMagicBell(); usePushNotificationHandler(); + useEffect(() => { + fetchNotifications(); + }, [fetchNotifications]); + return ( - {store?.notifications.map((notification) => ( + {isLoading && } + {error && Error: {error.message}} + {notifications?.map((notification) => ( ))} diff --git a/src/screens/SignIn.tsx b/src/screens/SignIn.tsx index 9b0ee3d..5c44111 100644 --- a/src/screens/SignIn.tsx +++ b/src/screens/SignIn.tsx @@ -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 [secretKey, setSecretKey] = useState(defaultCredentials.secretKey); + const [userJWT, setUserJWT] = useState(defaultCredentials.userJWT); useEffect(() => { if (reviewCredentials) { setServerURL(reviewCredentials.serverURL); - setApiKey(reviewCredentials.apiKey); - setUserEmail(reviewCredentials.userEmail); - setSecretKey(reviewCredentials.secretKey); + setUserJWT(reviewCredentials.userJWT); } }, [reviewCredentials]); const handleSubmit = useCallback(async () => { setLoading(true); - await signIn({ apiKey, userEmail, secretKey, serverURL }); + await signIn({ serverURL, userJWT }); setLoading(false); - }, [signIn, apiKey, userEmail, secretKey, serverURL]); + }, [signIn, userJWT, serverURL]); if (credentials) { throw new Error('User is already signed in'); @@ -75,9 +71,6 @@ export const SignInScreen = (): React.JSX.Element => { onValueChange={ ((itemValue: keyof typeof config) => { const c = config[itemValue]; - setApiKey(c.apiKey); - setUserEmail(c.userEmail); - setSecretKey(c.secretKey); setServerURL(c.serverURL); }) as (itemValue: string) => void } @@ -87,9 +80,7 @@ export const SignInScreen = (): React.JSX.Element => { })} - - - + diff --git a/yarn.lock b/yarn.lock index 7960124..d3dbdb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3421,7 +3421,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.2.3, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: +base64-js@^1.2.3, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -4453,13 +4453,6 @@ expo-constants@~17.0.4: "@expo/config" "~10.0.8" "@expo/env" "~0.4.1" -expo-crypto@~14.0.1: - version "14.0.2" - resolved "https://registry.yarnpkg.com/expo-crypto/-/expo-crypto-14.0.2.tgz#5f5d83c849164229f7a3e6a341887142756d517e" - integrity sha512-WRc9PBpJraJN29VD5Ef7nCecxJmZNyRKcGkNiDQC1nhY5agppzwhqh7zEzNFarE/GqDgSiaDHS8yd5EgFhP9AQ== - dependencies: - base64-js "^1.3.0" - expo-dev-client@~5.0.20: version "5.0.20" resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-5.0.20.tgz#349a6251d1d63c3142ad5232be653038b5c6cf15" From f22b912e9f0732b602e65d3bf8422259e72fc951 Mon Sep 17 00:00:00 2001 From: Niya Gupta Date: Tue, 18 Nov 2025 16:16:39 +0530 Subject: [PATCH 4/8] fix: apns and fcm payloads --- src/hooks/useDeviceToken.tsx | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/hooks/useDeviceToken.tsx b/src/hooks/useDeviceToken.tsx index 79e00ff..415b853 100644 --- a/src/hooks/useDeviceToken.tsx +++ b/src/hooks/useDeviceToken.tsx @@ -5,7 +5,7 @@ import { getIosPushNotificationServiceEnvironmentAsync, } from 'expo-application'; import { getDevicePushTokenAsync, requestPermissionsAsync } from 'expo-notifications'; -import { ApnsTokenPayload, ApnsTokenPayloadInstallationId, Client } from 'magicbell-js/user-client'; +import { ApnsTokenPayload, ApnsTokenPayloadInstallationId, Client, FcmTokenPayload } from 'magicbell-js/user-client'; import React, { useEffect } from 'react'; import { Platform } from 'react-native'; import { Credentials } from './useAuth'; @@ -15,26 +15,24 @@ const clientWithCredentials = (credentials: Credentials) => token: credentials.userJWT, }); -const apnsTokenPayload = async (token: string): Promise => { +const apnsTokenPayload = async (token: string): Promise => { const isSimulator = (await getIosApplicationReleaseTypeAsync()) === ApplicationReleaseType.SIMULATOR; const installationId = (await getIosPushNotificationServiceEnvironmentAsync()) || isSimulator ? ApnsTokenPayloadInstallationId.DEVELOPMENT : ApnsTokenPayloadInstallationId.PRODUCTION; + + const appId = applicationId ?? undefined; return { - apns: { - deviceToken: token, - installationId, - appId: applicationId, - }, + deviceToken: token, + installationId, + appId, }; }; -const fcmTokenPayload = (token: string): any => { +const fcmTokenPayload = (token: string): FcmTokenPayload => { return { - fcm: { - deviceToken: token, - }, + deviceToken: token, }; }; From f52bb8ce096dee2b05bf0eadc658ef49b302fde7 Mon Sep 17 00:00:00 2001 From: Niya Gupta Date: Wed, 19 Nov 2025 14:47:54 +0530 Subject: [PATCH 5/8] fix: pass server url to user-client --- src/components/MagicBellProvider.tsx | 46 +++++++++------------------- src/constants.ts | 5 +-- src/hooks/useAuth.tsx | 6 ++-- src/hooks/useDeviceToken.tsx | 1 + src/hooks/useReviewCredentials.tsx | 2 +- src/screens/SignIn.tsx | 1 + 6 files changed, 23 insertions(+), 38 deletions(-) diff --git a/src/components/MagicBellProvider.tsx b/src/components/MagicBellProvider.tsx index 9cf79e1..0442acf 100644 --- a/src/components/MagicBellProvider.tsx +++ b/src/components/MagicBellProvider.tsx @@ -1,11 +1,4 @@ -import React, { - createContext, - useContext, - useState, - useCallback, - useMemo, - ReactNode, -} 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'; @@ -28,9 +21,7 @@ type MagicBellContextType = { archiveNotification: (notificationId: string) => Promise; }; -const MagicBellContext = createContext( - undefined -); +const MagicBellContext = createContext(undefined); export const useMagicBell = () => { const context = useContext(MagicBellContext); @@ -57,6 +48,7 @@ export default function MagicBellProvider({ children }: MagicBellProviderProps) } return new Client({ token: credentials.userJWT, + baseUrl: credentials.serverURL, }); }, [credentials?.userJWT]); @@ -96,7 +88,7 @@ export default function MagicBellProvider({ children }: MagicBellProviderProps) setIsLoading(false); } }, - [client] + [client], ); const refreshNotifications = useCallback(async () => { @@ -113,17 +105,15 @@ export default function MagicBellProvider({ children }: MagicBellProviderProps) // Optimistically update local state setNotifications((prev) => prev.map((notification) => - notification.id === notificationId - ? { ...notification, readAt: new Date().toISOString() } - : notification - ) + notification.id === notificationId ? { ...notification, readAt: new Date().toISOString() } : notification, + ), ); } catch (err) { console.error('Error marking notification as read:', err); throw err; } }, - [client] + [client], ); const markAsUnread = useCallback( @@ -136,17 +126,15 @@ export default function MagicBellProvider({ children }: MagicBellProviderProps) // Optimistically update local state setNotifications((prev) => prev.map((notification) => - notification.id === notificationId - ? { ...notification, readAt: null } - : notification - ) + notification.id === notificationId ? { ...notification, readAt: null } : notification, + ), ); } catch (err) { console.error('Error marking notification as unread:', err); throw err; } }, - [client] + [client], ); const archiveNotification = useCallback( @@ -157,15 +145,13 @@ export default function MagicBellProvider({ children }: MagicBellProviderProps) await client.notifications.archiveNotification(notificationId); // Optimistically update local state - setNotifications((prev) => - prev.filter((notification) => notification.id !== notificationId) - ); + setNotifications((prev) => prev.filter((notification) => notification.id !== notificationId)); } catch (err) { console.error('Error archiving notification:', err); throw err; } }, - [client] + [client], ); const value = useMemo( @@ -190,12 +176,8 @@ export default function MagicBellProvider({ children }: MagicBellProviderProps) markAsRead, markAsUnread, archiveNotification, - ] + ], ); - return ( - - {children} - - ); + return {children}; } diff --git a/src/constants.ts b/src/constants.ts index e20d6ad..4efad5c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -75,8 +75,9 @@ export const routes = { export const config: { [key: string]: Credentials } = { prod: { - serverURL: 'https://api.magicbell.com', - userJWT: '', + serverURL: 'https://api.magicbell.com/v2', + userJWT: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2VtYWlsIjpudWxsLCJ1c2VyX2V4dGVybmFsX2lkIjoiN2Y0YmFhYjUtMGM5MS00NGU4LThiNTgtNWZmODQ5NTM1MTc0IiwiYXBpX2tleSI6IjVmNWNmYjI5NTEzODQ2NDMzZTgxYjkxZWM1ZTkwOGM5NDNmZjYwNTgiLCJpYXQiOjE3NjM1NDMyOTksImV4cCI6MTc2MzYyOTY5OX0.NzZcuIv_g-nW0JAhF0i_pH4T96BHCfkdjkJOLnqvF6M', }, local: { serverURL: 'https://1b35-79-153-3-135.ngrok-free.app', diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index a8a7115..27b0984 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -6,7 +6,6 @@ import useDeviceToken from './useDeviceToken'; const storageKey = 'mb'; -// TODO: refactor to remove other Credientials fields except userJWT and serverURL export type Credentials = { serverURL: string; userJWT: string; @@ -63,16 +62,17 @@ const getCredentials = async () => { return null; } try { - const { apiKey, secretKey, userEmail, serverURL, userJWT } = JSON.parse(value); + const { serverURL, userJWT } = JSON.parse(value); const client = new Client({ token: userJWT, + baseUrl: serverURL, }); // TODO: Verify bad credentials cannot be used // Use the client to check the credentials are valid if (client.config) { - return { apiKey, userEmail, secretKey, serverURL, userJWT }; + return { serverURL, userJWT }; } } catch (e) { console.error('Error parsing credentials', e); diff --git a/src/hooks/useDeviceToken.tsx b/src/hooks/useDeviceToken.tsx index 415b853..16583ab 100644 --- a/src/hooks/useDeviceToken.tsx +++ b/src/hooks/useDeviceToken.tsx @@ -13,6 +13,7 @@ import { Credentials } from './useAuth'; const clientWithCredentials = (credentials: Credentials) => new Client({ token: credentials.userJWT, + baseUrl: credentials.serverURL, }); const apnsTokenPayload = async (token: string): Promise => { diff --git a/src/hooks/useReviewCredentials.tsx b/src/hooks/useReviewCredentials.tsx index a9c0cf3..b495d82 100644 --- a/src/hooks/useReviewCredentials.tsx +++ b/src/hooks/useReviewCredentials.tsx @@ -10,7 +10,7 @@ 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=[...]&secretKey=[...]&userEmail=[...]&userJWT=[...] + * x-magicbell-review://connect?apiHost=[...]&userJWT=[...] * */ const parseLaunchURLCredentials = (url: URL): Credentials | null => { diff --git a/src/screens/SignIn.tsx b/src/screens/SignIn.tsx index 5c44111..d6adf50 100644 --- a/src/screens/SignIn.tsx +++ b/src/screens/SignIn.tsx @@ -72,6 +72,7 @@ export const SignInScreen = (): React.JSX.Element => { ((itemValue: keyof typeof config) => { const c = config[itemValue]; setServerURL(c.serverURL); + setUserJWT(c.userJWT); }) as (itemValue: string) => void } > From 3982298407fee06c22598550a7682f6e74f8369f Mon Sep 17 00:00:00 2001 From: Niya Gupta Date: Wed, 19 Nov 2025 16:34:25 +0530 Subject: [PATCH 6/8] refactor: MagicBellProvider types, remove unused imports --- src/components/Button.tsx | 2 +- src/components/MagicBellProvider.tsx | 36 ++++++++++------------------ src/hooks/useReviewCredentials.tsx | 2 +- 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 501f5b7..fb9dfed 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -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'; diff --git a/src/components/MagicBellProvider.tsx b/src/components/MagicBellProvider.tsx index 0442acf..dac8cde 100644 --- a/src/components/MagicBellProvider.tsx +++ b/src/components/MagicBellProvider.tsx @@ -2,19 +2,21 @@ import React, { createContext, useContext, useState, useCallback, useMemo, React import { Client, Notification } from 'magicbell-js/user-client'; import { useCredentials } from '../hooks/useAuth'; +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?: { - limit?: number; - startingAfter?: string; - endingBefore?: string; - status?: string; - category?: string; - topic?: string; - }) => Promise; + fetchNotifications: (params?: ListNotificationsParams) => Promise; refreshNotifications: () => Promise; markAsRead: (notificationId: string) => Promise; markAsUnread: (notificationId: string) => Promise; @@ -41,7 +43,6 @@ export default function MagicBellProvider({ children }: MagicBellProviderProps) const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - // Create client with JWT token const client = useMemo(() => { if (!credentials?.userJWT) { return null; @@ -50,17 +51,10 @@ export default function MagicBellProvider({ children }: MagicBellProviderProps) token: credentials.userJWT, baseUrl: credentials.serverURL, }); - }, [credentials?.userJWT]); + }, [credentials?.userJWT, credentials?.serverURL]); const fetchNotifications = useCallback( - async (params?: { - limit?: number; - startingAfter?: string; - endingBefore?: string; - status?: string; - category?: string; - topic?: string; - }) => { + async (params?: ListNotificationsParams) => { if (!client) { setError(new Error('MagicBell client not initialized')); return; @@ -75,14 +69,11 @@ export default function MagicBellProvider({ children }: MagicBellProviderProps) ...params, }); - // The SDK returns HttpResponse - // NotificationCollection has an optional data field 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); - // Ensure notifications is set to empty array on error setNotifications([]); } finally { setIsLoading(false); @@ -102,7 +93,6 @@ export default function MagicBellProvider({ children }: MagicBellProviderProps) try { await client.notifications.markNotificationRead(notificationId); - // Optimistically update local state setNotifications((prev) => prev.map((notification) => notification.id === notificationId ? { ...notification, readAt: new Date().toISOString() } : notification, @@ -123,7 +113,6 @@ export default function MagicBellProvider({ children }: MagicBellProviderProps) try { await client.notifications.markNotificationUnread(notificationId); - // Optimistically update local state setNotifications((prev) => prev.map((notification) => notification.id === notificationId ? { ...notification, readAt: null } : notification, @@ -144,7 +133,6 @@ export default function MagicBellProvider({ children }: MagicBellProviderProps) try { await client.notifications.archiveNotification(notificationId); - // Optimistically update local state setNotifications((prev) => prev.filter((notification) => notification.id !== notificationId)); } catch (err) { console.error('Error archiving notification:', err); diff --git a/src/hooks/useReviewCredentials.tsx b/src/hooks/useReviewCredentials.tsx index b495d82..d14e009 100644 --- a/src/hooks/useReviewCredentials.tsx +++ b/src/hooks/useReviewCredentials.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Credentials } from './useAuth'; import { useURLMemo } from './useURLMemo'; From 648c4926ef1918e7a227a501c920c288c15965cc Mon Sep 17 00:00:00 2001 From: Niya Gupta Date: Fri, 21 Nov 2025 14:49:47 +0530 Subject: [PATCH 7/8] chore: update readme and google-services.json --- README.md | 20 +++++++++++++++++++- google-services.json | 29 +++++------------------------ 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 3373878..9ef7819 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ 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`. @@ -103,6 +103,24 @@ 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. + +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 diff --git a/google-services.json b/google-services.json index ec45d35..f447205 100644 --- a/google-services.json +++ b/google-services.json @@ -1,13 +1,13 @@ { "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" } @@ -15,26 +15,7 @@ "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": { From 624a18984b227119c84fd592cd83f2595e8f9cbd Mon Sep 17 00:00:00 2001 From: Niya Gupta Date: Fri, 21 Nov 2025 15:00:19 +0530 Subject: [PATCH 8/8] fix: add instructions for authenticating user --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9ef7819..ca69fd3 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,8 @@ 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.