From b9f6f2dee7c59408fbee9b5ad63e508a3b2cf682 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Thu, 7 May 2026 12:01:06 -0600 Subject: [PATCH 01/13] feat: redesign auth page, add deploy workflow, update CI - Rewrite example Auth page with settings-style environment/URL rows, edit dialogs, Save button, and AsyncStorage persistence (matching Flutter example UX) - Add environment presets (Production, Production EU, Staging, Dev, Custom) with full URL sets - Update ExampleServer.generateJwt/sendTest to accept baseUrl param - Add .github/workflows/deploy.yml for automated npm publish, git tag, and GitHub Release on push to main - Update CI workflow to use actions/checkout@v4, cache@v4, setup-java@v4 - Fix workflow.yml trigger branch from master to main - Update composite setup action to use setup-node@v4 and cache@v4 - Install @react-native-async-storage/async-storage and @react-native-clipboard/clipboard in example app Co-authored-by: Cursor --- .github/actions/setup/action.yml | 4 +- .github/workflows/ci.yml | 22 +- .github/workflows/deploy.yml | 99 ++++ .github/workflows/workflow.yml | 4 +- example/package.json | 6 +- example/src/AuthPreferences.ts | 72 +++ example/src/CourierEnvironment.ts | 67 +++ example/src/Home.tsx | 46 +- example/src/Utils.tsx | 62 +-- example/src/pages/Auth.tsx | 738 +++++++++++++++++++----------- example/yarn.lock | 17 + 11 files changed, 806 insertions(+), 331 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 example/src/AuthPreferences.ts create mode 100644 example/src/CourierEnvironment.ts diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index f918c91..ef95e58 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -5,13 +5,13 @@ runs: using: composite steps: - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: .nvmrc - name: Cache dependencies id: yarn-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/node_modules diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39efde8..36a0ac3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup @@ -53,13 +53,13 @@ jobs: TURBO_CACHE_DIR: .turbo/android steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup - name: Cache turborepo for Android - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.TURBO_CACHE_DIR }} key: ${{ runner.os }}-turborepo-android-${{ hashFiles('**/yarn.lock') }} @@ -76,10 +76,10 @@ jobs: - name: Install JDK if: env.turbo_cache_hit != 1 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' - java-version: '11' + java-version: '17' - name: Finalize Android SDK if: env.turbo_cache_hit != 1 @@ -88,7 +88,7 @@ jobs: - name: Cache Gradle if: env.turbo_cache_hit != 1 - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/wrapper @@ -107,13 +107,13 @@ jobs: TURBO_CACHE_DIR: .turbo/ios steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup - name: Cache turborepo for iOS - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.TURBO_CACHE_DIR }} key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('**/yarn.lock') }} @@ -131,7 +131,7 @@ jobs: - name: Cache cocoapods if: env.turbo_cache_hit != 1 id: cocoapods-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/ios/Pods diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..76a8e26 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,99 @@ +name: Deploy + +on: + push: + branches: [main] + +jobs: + deploy: + name: Deploy to npm + runs-on: ubuntu-latest + permissions: + contents: write + outputs: + deployed: ${{ steps.check.outputs.deployed }} + version: ${{ steps.check.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + registry-url: 'https://registry.npmjs.org' + + - name: Check version + id: check + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION" + + PUBLISHED=$(npm view @trycourier/courier-react-native versions --json 2>/dev/null || echo "[]") + if echo "$PUBLISHED" | python3 -c "import sys,json; vs=json.load(sys.stdin); sys.exit(0 if '$VERSION' in vs else 1)" 2>/dev/null; then + echo "Version $VERSION already published on npm. Skipping deploy." + echo "deployed=false" >> "$GITHUB_OUTPUT" + else + echo "Version $VERSION not yet published. Deploying." + echo "deployed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Install dependencies + if: steps.check.outputs.deployed == 'true' + run: yarn install --frozen-lockfile + + - name: Build package + if: steps.check.outputs.deployed == 'true' + run: yarn prepack + + - name: Create git tag + if: steps.check.outputs.deployed == 'true' + run: | + VERSION="${{ steps.check.outputs.version }}" + git tag "$VERSION" + git push origin "$VERSION" + + - name: Generate release notes + if: steps.check.outputs.deployed == 'true' + id: notes + run: | + PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || git rev-list --max-parents=0 HEAD) + NOTES=$(git log "$PREVIOUS_TAG"..HEAD --pretty=format:"- %s" --no-merges) + echo "notes<> "$GITHUB_OUTPUT" + echo "$NOTES" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + - name: Create GitHub release + if: steps.check.outputs.deployed == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION="${{ steps.check.outputs.version }}" + gh release create "$VERSION" --notes "${{ steps.notes.outputs.notes }}" + + - name: Publish to npm + if: steps.check.outputs.deployed == 'true' + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + + - name: Setup + uses: ./.github/actions/setup + + - name: Run unit tests + run: yarn test --maxWorkers=2 --coverage diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 983593c..35afcf4 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -2,10 +2,10 @@ name: Test Trigger on: push: - branches: [master] + branches: [main] workflow_dispatch: pull_request: - branches: [master] + branches: [main] jobs: tests-on-push: diff --git a/example/package.json b/example/package.json index bb1fbc6..09207c2 100644 --- a/example/package.json +++ b/example/package.json @@ -10,11 +10,14 @@ "setupEnv": "if [[ ! -e .env ]] ; then cp .env.sample .env ; fi" }, "dependencies": { + "@react-native-async-storage/async-storage": "^3.0.2", + "@react-native-clipboard/clipboard": "^1.16.3", "@react-native-segmented-control/segmented-control": "^2.5.2", "@react-navigation/bottom-tabs": "^6.5.20", "@react-navigation/material-top-tabs": "^6.6.13", "@react-navigation/native": "^6.1.17", "@react-navigation/stack": "^6.3.29", + "@trycourier/courier-react-native": "link:..", "eventemitter3": "^5.0.1", "react": "18.2.0", "react-native": "0.73.7", @@ -23,8 +26,7 @@ "react-native-safe-area-context": "^4.9.0", "react-native-screens": "^3.30.1", "react-native-tab-view": "^3.5.2", - "react-native-vector-icons": "^10.0.0", - "@trycourier/courier-react-native": "link:.." + "react-native-vector-icons": "^10.0.0" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/example/src/AuthPreferences.ts b/example/src/AuthPreferences.ts new file mode 100644 index 0000000..cd71387 --- /dev/null +++ b/example/src/AuthPreferences.ts @@ -0,0 +1,72 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { CourierEnvironment, DEFAULT_URLS } from './CourierEnvironment'; +import Env from './Env'; + +const KEYS = { + environment: '@auth_environment', + userId: '@auth_userId', + tenantId: '@auth_tenantId', + apiKey: '@auth_apiKey', + restUrl: '@auth_restUrl', + graphqlUrl: '@auth_graphqlUrl', + inboxGraphqlUrl: '@auth_inboxGraphqlUrl', + inboxWebSocketUrl: '@auth_inboxWebSocketUrl', +} as const; + +export interface AuthPreferencesData { + environment: CourierEnvironment; + userId: string; + tenantId: string; + apiKey: string; + restUrl: string; + graphqlUrl: string; + inboxGraphqlUrl: string; + inboxWebSocketUrl: string; +} + +export async function loadAuthPreferences(): Promise { + const results = await AsyncStorage.getMany([ + KEYS.environment, + KEYS.userId, + KEYS.tenantId, + KEYS.apiKey, + KEYS.restUrl, + KEYS.graphqlUrl, + KEYS.inboxGraphqlUrl, + KEYS.inboxWebSocketUrl, + ]); + + return { + environment: + (results[KEYS.environment] as CourierEnvironment) ?? + CourierEnvironment.Production, + userId: results[KEYS.userId] ?? '', + tenantId: results[KEYS.tenantId] ?? '', + apiKey: results[KEYS.apiKey] ?? Env.authKey, + restUrl: results[KEYS.restUrl] ?? DEFAULT_URLS.rest, + graphqlUrl: results[KEYS.graphqlUrl] ?? DEFAULT_URLS.graphql, + inboxGraphqlUrl: results[KEYS.inboxGraphqlUrl] ?? DEFAULT_URLS.inboxGraphql, + inboxWebSocketUrl: + results[KEYS.inboxWebSocketUrl] ?? DEFAULT_URLS.inboxWebSocket, + }; +} + +export async function saveAuthPreferences( + data: Partial +): Promise { + const entries: Record = {}; + if (data.environment !== undefined) + entries[KEYS.environment] = data.environment; + if (data.userId !== undefined) entries[KEYS.userId] = data.userId; + if (data.tenantId !== undefined) entries[KEYS.tenantId] = data.tenantId; + if (data.apiKey !== undefined) entries[KEYS.apiKey] = data.apiKey; + if (data.restUrl !== undefined) entries[KEYS.restUrl] = data.restUrl; + if (data.graphqlUrl !== undefined) entries[KEYS.graphqlUrl] = data.graphqlUrl; + if (data.inboxGraphqlUrl !== undefined) + entries[KEYS.inboxGraphqlUrl] = data.inboxGraphqlUrl; + if (data.inboxWebSocketUrl !== undefined) + entries[KEYS.inboxWebSocketUrl] = data.inboxWebSocketUrl; + if (Object.keys(entries).length > 0) { + await AsyncStorage.setMany(entries); + } +} diff --git a/example/src/CourierEnvironment.ts b/example/src/CourierEnvironment.ts new file mode 100644 index 0000000..581adc2 --- /dev/null +++ b/example/src/CourierEnvironment.ts @@ -0,0 +1,67 @@ +import { CourierApiUrls } from '@trycourier/courier-react-native'; + +export interface CourierEnvironmentUrls { + rest: string; + graphql: string; + inboxGraphql: string; + inboxWebSocket: string; +} + +export enum CourierEnvironment { + Production = 'Production', + ProductionEU = 'Production EU', + Staging = 'Staging', + Dev = 'Dev', + Custom = 'Custom', +} + +const environmentUrls: Record = { + [CourierEnvironment.Production]: { + rest: 'https://api.courier.com', + graphql: 'https://api.courier.com/client/q', + inboxGraphql: 'https://inbox.courier.com/q', + inboxWebSocket: 'wss://realtime.courier.io', + }, + [CourierEnvironment.ProductionEU]: { + rest: 'https://api.eu.courier.com', + graphql: 'https://api.eu.courier.com/client/q', + inboxGraphql: 'https://inbox.eu.courier.io/q', + inboxWebSocket: 'wss://realtime.eu.courier.io', + }, + [CourierEnvironment.Staging]: { + rest: 'https://api.courierstaging.com', + graphql: 'https://api.courierstaging.com/client/q', + inboxGraphql: 'http://inbox.courierstaging.com/', + inboxWebSocket: + 'wss://inbox-staging-ws-alb-490231599.us-east-1.elb.amazonaws.com', + }, + [CourierEnvironment.Dev]: { + rest: 'https://api.courierdev.com', + graphql: 'https://api.courierdev.com/client/q', + inboxGraphql: 'https://inbox.courierdev.com/q', + inboxWebSocket: 'wss://9mrugsdnk1.execute-api.us-east-1.amazonaws.com/dev', + }, +}; + +export const DEFAULT_URLS: CourierEnvironmentUrls = { + rest: 'https://api.courier.com', + graphql: 'https://api.courier.com/client/q', + inboxGraphql: 'https://inbox.courier.com/q', + inboxWebSocket: 'wss://realtime.courier.io', +}; + +export function getUrlsForEnvironment( + env: CourierEnvironment +): CourierEnvironmentUrls | null { + if (env === CourierEnvironment.Custom) return null; + return environmentUrls[env] ?? null; +} + +export function toApiUrls(urls: CourierEnvironmentUrls): CourierApiUrls { + return { + rest: urls.rest, + graphql: urls.graphql, + inboxGraphql: urls.inboxGraphql, + inboxWebSocket: urls.inboxWebSocket, + }; +} diff --git a/example/src/Home.tsx b/example/src/Home.tsx index 58b2cb0..b408b25 100644 --- a/example/src/Home.tsx +++ b/example/src/Home.tsx @@ -1,5 +1,8 @@ import React, { useEffect, useState } from 'react'; -import { BottomTabNavigationOptions, createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { + BottomTabNavigationOptions, + createBottomTabNavigator, +} from '@react-navigation/bottom-tabs'; import { Alert, Button } from 'react-native'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import Auth from './pages/Auth'; @@ -12,14 +15,15 @@ import Tests from './pages/Tests'; const Tab = createBottomTabNavigator(); const Home = () => { - const [unreadCount, setUnreadCount] = useState(0); useEffect(() => { const setupInbox = async () => { // Setup Push - Courier.setIOSForegroundPresentationOptions({ options: ['sound', 'badge', 'list', 'banner'] }); + Courier.setIOSForegroundPresentationOptions({ + options: ['sound', 'badge', 'list', 'banner'], + }); const pushListener = Courier.shared.addPushNotificationListener({ onPushNotificationClicked(push) { @@ -29,7 +33,7 @@ const Home = () => { onPushNotificationDelivered(push) { console.log(push); Alert.alert('๐Ÿ“ฌ Push Notification Delivered', JSON.stringify(push)); - } + }, }); // Setup Inbox @@ -53,14 +57,12 @@ const Home = () => { inboxListener.remove(); }); }; - }, []); const inboxOptions = (): BottomTabNavigationOptions => { - const badgeCount = () => { return unreadCount > 0 ? unreadCount : undefined; - } + }; return { headerRight: () => ( @@ -72,24 +74,36 @@ const Home = () => { tabBarBadge: badgeCount(), tabBarIcon: ({ color, size }) => ( - ) - } - } + ), + }; + }; const icon = (icon: string): BottomTabNavigationOptions => { return { tabBarIcon: ({ color, size }) => ( - ) - } - } + ), + }; + }; return ( - - + + - + ); diff --git a/example/src/Utils.tsx b/example/src/Utils.tsx index 7a219a5..c4d5aa8 100644 --- a/example/src/Utils.tsx +++ b/example/src/Utils.tsx @@ -3,55 +3,63 @@ interface Response { } export class ExampleServer { - - public static async generateJwt(props: { authKey: string, userId: string }): Promise { - + public static async generateJwt(props: { + authKey: string; + userId: string; + baseUrl?: string; + }): Promise { return new Promise((resolve, reject) => { - - const url = 'https://api.courier.com/auth/issue-token'; + const base = props.baseUrl ?? 'https://api.courier.com'; + const url = `${base}/auth/issue-token`; const request = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${props.authKey}` + 'Authorization': `Bearer ${props.authKey}`, }, body: JSON.stringify({ scope: `user_id:${props.userId} write:user-tokens inbox:read:messages inbox:write:events read:preferences write:preferences read:brands`, - expires_in: '2 days' - }) + expires_in: '2 days', + }), }; fetch(url, request) - .then(response => response.json()) + .then((response) => response.json()) .then((data: Response) => { resolve(data.token); }) - .catch(error => { + .catch((error) => { reject(error); }); - }); - } - public static async sendTest(props: { authKey: string, userId: string, channel: string, title?: string, body?: string }): Promise { - const url = 'https://api.courier.com/send'; + public static async sendTest(props: { + authKey: string; + userId: string; + channel: string; + title?: string; + body?: string; + baseUrl?: string; + }): Promise { + const base = props.baseUrl ?? 'https://api.courier.com'; + const url = `${base}/send`; const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${props.authKey}`, }; const body = JSON.stringify({ - 'message': { - 'to': { - 'user_id': props.userId + message: { + to: { + user_id: props.userId, }, - 'content': { - 'title': props.title ?? 'Test', - 'body': props.body ?? 'Body', + content: { + title: props.title ?? 'Test', + body: props.body ?? 'Body', }, - 'routing': { - 'method': 'all', - 'channels': [props.channel], + routing: { + method: 'all', + channels: [props.channel], }, }, }); @@ -69,19 +77,17 @@ export class ExampleServer { throw new Error('Failed to send test message'); } } - } export class Utils { - static generateUUID(): string { let uuid = ''; - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const charactersLength = characters.length; for (let i = 0; i < 16; i++) { uuid += characters.charAt(Math.floor(Math.random() * charactersLength)); } return uuid; } - -} \ No newline at end of file +} diff --git a/example/src/pages/Auth.tsx b/example/src/pages/Auth.tsx index c9ac9f2..25e7bc8 100644 --- a/example/src/pages/Auth.tsx +++ b/example/src/pages/Auth.tsx @@ -1,311 +1,509 @@ -import Courier from "@trycourier/courier-react-native"; -import React, { useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Button, Modal, Platform, StyleSheet, Text, TextInput, TouchableOpacity, View } from "react-native"; -import Env from "../Env"; -import { ExampleServer } from "../Utils"; -import { usePoke } from '../Poke'; +import Courier from '@trycourier/courier-react-native'; +import React, { useCallback, useEffect, useState } from 'react'; +import { + ActivityIndicator, + Alert, + FlatList, + Modal, + Platform, + Pressable, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; +import Clipboard from '@react-native-clipboard/clipboard'; +import { ExampleServer } from '../Utils'; +import { + CourierEnvironment, + getUrlsForEnvironment, + toApiUrls, +} from '../CourierEnvironment'; +import { + AuthPreferencesData, + loadAuthPreferences, + saveAuthPreferences, +} from '../AuthPreferences'; + +const MONO_FONT = Platform.select({ + ios: 'Courier', + android: 'monospace', + default: 'monospace', +}); + +interface OptionRow { + key: string; + value: string; +} const Auth = () => { - - const [isLoading, setIsLoading] = useState(false) - const [userId, setUserId] = useState() - const [tenantId, setTenantId] = useState() + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [options, setOptions] = useState([]); + const [selectedEnvironment, setSelectedEnvironment] = + useState(CourierEnvironment.Production); + const [envPickerVisible, setEnvPickerVisible] = useState(false); + const [editModalVisible, setEditModalVisible] = useState(false); + const [editIndex, setEditIndex] = useState(-1); + const [editValue, setEditValue] = useState(''); + + const buildOptions = useCallback( + (prefs: AuthPreferencesData): OptionRow[] => { + return [ + { key: 'Environment', value: prefs.environment }, + { key: 'User ID', value: prefs.userId }, + { key: 'Tenant ID (Optional)', value: prefs.tenantId }, + { key: 'API Key', value: prefs.apiKey }, + { key: 'REST URL', value: prefs.restUrl }, + { key: 'GraphQL URL', value: prefs.graphqlUrl }, + { key: 'Inbox GraphQL URL', value: prefs.inboxGraphqlUrl }, + { key: 'Inbox WebSocket', value: prefs.inboxWebSocketUrl }, + ]; + }, + [] + ); useEffect(() => { - const initAuth = async () => { - const authListener = await Courier.shared.addAuthenticationListener({ - onUserChanged: async (userId) => { - setUserId(userId); - setTenantId(await Courier.shared.getTenantId()); - console.log(`User changed: ${userId}`); - console.log(`Tenant changed: ${await Courier.shared.getTenantId()}`); - } - }); + let authListener: any; - const userId = await Courier.shared.getUserId(); - const tenantId = await Courier.shared.getTenantId(); - console.log(`Initial user: ${userId}`); - console.log(`Initial tenant: ${tenantId}`); - refreshJWT(userId, tenantId); + const init = async () => { + const prefs = await loadAuthPreferences(); + setSelectedEnvironment(prefs.environment); + setOptions(buildOptions(prefs)); + setIsLoading(false); - return authListener; + authListener = await Courier.shared.addAuthenticationListener({ + onUserChanged: async (_userId) => { + // Refresh UI state when auth changes externally + }, + }); }; - let listener: any; - initAuth().then(result => { - listener = result; - }); + init(); return () => { - if (listener) { - listener.remove(); - } + authListener?.remove(); }; + }, [buildOptions]); - }, []); - - async function refreshJWT(userId?: string, tenantId?: string) { - - console.log('Refreshing JWT'); - - if (!userId) { - console.log(`No user found`); - setIsLoading(false); - return; - } - - setIsLoading(true); + const saveEnabled = options.length > 1 && options[1]!.value.length > 0; + const performSave = async () => { + setIsSaving(true); try { - - console.log(`User ID: ${userId}`); - - await Courier.shared.signOut(); - - const token = await ExampleServer.generateJwt({ - authKey: Env.authKey, - userId: userId, - }); - - console.log(`New token: ${token}`); - - await Courier.shared.signIn({ - accessToken: token, - userId: userId, - tenantId: tenantId, - }); - - } catch (e) { - - console.error(e); - + if (await Courier.shared.getUserId()) { + await Courier.shared.signOut(); + } + await performSignIn(); + } catch (e: any) { + Alert.alert('Error', e?.message ?? String(e)); } + setIsSaving(false); + }; - setIsLoading(false); - - } - - async function signIn(userId: string, tenantId: string) { - - console.log('Signing User In'); - console.log(`User ID: ${userId}`); - console.log(`Tenant ID: ${tenantId}`); - - setIsLoading(true); - - try { - - const token = await ExampleServer.generateJwt({ - authKey: Env.authKey, - userId: userId, - }); - - console.log(`New token: ${token}`); - - await Courier.shared.signIn({ - accessToken: token, - userId: userId, - tenantId: tenantId.length ? tenantId : undefined - }); - - setUserId(await Courier.shared.getUserId()); - setTenantId(await Courier.shared.getTenantId()); - - } catch (e) { - - console.error(e); + const performSignIn = async () => { + const userId = options[1]!.value; + const tenantId = options[2]!.value || undefined; + const apiKey = options[3]!.value; + const restUrl = options[4]!.value; + if (!userId) { + await Courier.shared.signOut(); + return; } - setIsLoading(false); - - } - - async function signOut() { - await Courier.shared.signOut(); - setUserId(await Courier.shared.getUserId()); - setTenantId(await Courier.shared.getTenantId()); - } + const jwt = await ExampleServer.generateJwt({ + authKey: apiKey, + userId: userId, + baseUrl: restUrl, + }); - const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - text: { - marginBottom: 10, - fontFamily: Platform.select({ - ios: 'Courier', - android: 'monospace', - default: 'monospace', + await Courier.shared.signIn({ + userId: userId, + tenantId: tenantId, + accessToken: jwt, + apiUrls: toApiUrls({ + rest: options[4]!.value, + graphql: options[5]!.value, + inboxGraphql: options[6]!.value, + inboxWebSocket: options[7]!.value, }), - fontSize: 16, - }, - }); + }); + }; - const AuthButton = (props: { buttonText: string }) => { + const onRowTapped = (index: number) => { + if (index === 0) { + setEnvPickerVisible(true); + } else if ( + index >= 4 && + selectedEnvironment !== CourierEnvironment.Custom + ) { + // URL rows in non-custom mode: copy to clipboard + const val = options[index]!.value; + if (val) { + Clipboard.setString(val); + Alert.alert('Copied', val); + } + } else { + setEditIndex(index); + setEditValue(options[index]!.value); + setEditModalVisible(true); + } + }; - const [modalVisible, setModalVisible] = useState(false); - const [userId, setUserId] = useState(''); - const [tenantId, setTenantId] = useState(''); - const inputRef = useRef(null); + const applyEnvironment = async (env: CourierEnvironment) => { + setSelectedEnvironment(env); + const urls = getUrlsForEnvironment(env) ?? { + rest: options[4]!.value, + graphql: options[5]!.value, + inboxGraphql: options[6]!.value, + inboxWebSocket: options[7]!.value, + }; - const styles = StyleSheet.create({ - button: { - backgroundColor: 'lightgray', - padding: 10, - borderRadius: 5, - }, - buttonText: { - fontSize: 16, - fontFamily: Platform.select({ - ios: 'Courier', - android: 'monospace', - default: 'monospace', - }), - }, - modalContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.5)', - }, - modalContent: { - backgroundColor: 'white', - padding: 20, - borderRadius: 5, - elevation: 5, - minWidth: 300, - }, - input: { - borderWidth: 1, - borderColor: 'gray', - borderRadius: 5, - padding: 10, - marginBottom: 10, - }, + const updated = [...options]; + updated[0] = { key: 'Environment', value: env }; + updated[4] = { key: 'REST URL', value: urls.rest }; + updated[5] = { key: 'GraphQL URL', value: urls.graphql }; + updated[6] = { key: 'Inbox GraphQL URL', value: urls.inboxGraphql }; + updated[7] = { key: 'Inbox WebSocket', value: urls.inboxWebSocket }; + setOptions(updated); + + await saveAuthPreferences({ + environment: env, + restUrl: urls.rest, + graphqlUrl: urls.graphql, + inboxGraphqlUrl: urls.inboxGraphql, + inboxWebSocketUrl: urls.inboxWebSocket, }); + }; - useEffect(() => { - if (modalVisible) { - inputRef.current.focus(); - } - }, [modalVisible]); - - const handleButtonPress = async () => { - - if (await Courier.shared.getUserId()) { - await signOut(); - } else { - setModalVisible(true); - } + const handleEditSave = async () => { + const updated = [...options]; + updated[editIndex] = { ...updated[editIndex]!, value: editValue }; - }; - - const handleModalClose = () => { - setModalVisible(false); - }; - - const handleUserIdInputChange = (text: string) => { - setUserId(text); - }; + // If editing a URL row, switch to Custom env + if (editIndex >= 4 && selectedEnvironment !== CourierEnvironment.Custom) { + setSelectedEnvironment(CourierEnvironment.Custom); + updated[0] = { key: 'Environment', value: CourierEnvironment.Custom }; + await saveAuthPreferences({ environment: CourierEnvironment.Custom }); + } - const handleTenantIdInputChange = (text: string) => { - setTenantId(text); - }; - - const handleSaveButtonPress = () => { - signIn(userId, tenantId); - setModalVisible(false); + setOptions(updated); + setEditModalVisible(false); + + const prefKeyMap: Record = { + 1: 'userId', + 2: 'tenantId', + 3: 'apiKey', + 4: 'restUrl', + 5: 'graphqlUrl', + 6: 'inboxGraphqlUrl', + 7: 'inboxWebSocketUrl', }; - - return ( - <> - - - - Set Courier User Id: - - Set Tenant Id: - -