Skip to content

OBLS-688 Add server profiles feature with multi-env support#381

Open
olewandowski1 wants to merge 1 commit intodevelopfrom
OBLS-688
Open

OBLS-688 Add server profiles feature with multi-env support#381
olewandowski1 wants to merge 1 commit intodevelopfrom
OBLS-688

Conversation

@olewandowski1
Copy link
Copy Markdown
Collaborator

@olewandowski1 olewandowski1 commented Mar 30, 2026

https://openboxes.atlassian.net/browse/OBLS-688

Changes:

  • Add server profiles system allowing users to create, edit, delete, and switch between multiple server connections (dev, staging, production)
  • Display active profile on Login screen with tap-to-change navigation to Profiles management screen
  • Migrate existing API_URL from AsyncStorage into a "Default" profile on first boot after upgrade
  • Replace Server Connection card in Settings with a "Manage Profiles" link
  • Log user out and reset auth state when switching profiles
  • Use versioned AsyncStorage schema for forward-compatible profile data
Kazam_screencast_00057.mp4

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a server profiles system to support multiple backend environments (dev/staging/prod), including migration from the legacy single API_URL setting and UI entry points to manage/switch profiles.

Changes:

  • Introduces persisted “profiles” storage (versioned schema) with migration from legacy API_URL.
  • Adds a Profiles management screen + dialog for create/edit/delete/switch, including logout/reset on switch.
  • Updates Settings and Login screens to surface profile management and show the active profile.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/types/profile.ts Adds Profile + ProfileStorageData types for the new storage schema.
src/components/ProfileStorage.ts Implements AsyncStorage persistence, validation helpers, and legacy API_URL migration.
src/hooks/useProfiles.ts Adds a navigation-aware hook to load and mutate profile storage from screens.
src/screens/Profiles/index.tsx Implements Profiles list UI + activate/edit/delete flows and auth reset on switch.
src/screens/Profiles/ProfileFormDialog.tsx Adds modal form UI for creating/editing profiles with validation feedback.
src/screens/Profiles/styles.ts Styling for Profiles screen and dialog.
src/screens/Settings/index.tsx Replaces “Server Connection” URL editing with navigation to Profiles management.
src/screens/Login/index.tsx Displays the active profile card on Login with tap-to-change navigation.
src/screens/Login/styles.ts Adds styles for the active-profile card on Login.
src/Main.tsx Runs profile migration on navigator ready and sets ApiClient base URL or routes to Profiles.
src/redux/reducers/mainReducer.ts Resets main app state on LOGOUT_REQUEST_SUCCESS (used for profile switching).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +62 to +67
await addProfile(label, serverUrl);
}

setDialogVisible(false);
},
[editingProfile, activeProfileId, updateProfile, addProfile]
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When creating the very first profile (fresh install / no legacy API_URL), the new profile becomes active in storage but ApiClient never gets initialized with its base URL. Since ApiClient.client is only created via setBaseUrl, subsequent login requests will crash/fail until the user edits/switches a profile. Consider detecting the “first profile” case and calling ApiClient.setBaseUrl (and/or setting it active explicitly) right after creation so the app can log in immediately.

Suggested change
await addProfile(label, serverUrl);
}
setDialogVisible(false);
},
[editingProfile, activeProfileId, updateProfile, addProfile]
const isFirstProfile = profiles.length === 0;
await addProfile(label, serverUrl);
if (isFirstProfile) {
ApiClient.setBaseUrl(serverUrl);
}
}
setDialogVisible(false);
},
[editingProfile, activeProfileId, updateProfile, addProfile, profiles.length]

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +92
showPopup({
title: `Delete "${profile.label}"?`,
message: 'This profile will be permanently removed. This action cannot be undone.',
positiveButton: {
text: 'Delete',
callback: async () => {
const wasActive = profile.id === activeProfileId;
const success = await removeProfile(profile.id);
if (success && wasActive) {
const newActive = await ProfileStorage.getActiveProfile();
if (newActive) {
ApiClient.setBaseUrl(newActive.serverUrl);
}
}
}
},
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The confirm dialog’s positiveButton.callback is invoked synchronously by showPopup (Alert), and it does not await Promises. Using an async callback here means any thrown error becomes an unhandled rejected Promise and can be missed. Wrap the async work in a non-async callback that calls the async function and handles errors (e.g., with .catch(...)), or update the popup helper to support awaiting callbacks.

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +114
showPopup({
title: `Switch to "${profile.label}"?`,
message: 'You will be connected to a different server and will need to log in again.',
positiveButton: {
text: 'Switch',
callback: async () => {
await setActive(profile.id);
ApiClient.setBaseUrl(profile.serverUrl);
dispatch({ type: LOGOUT_REQUEST_SUCCESS });
NavigationService.reset('Login');
}
},
negativeButtonText: 'Cancel'
});
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: this async positiveButton callback returns a Promise that showPopup doesn’t await/handle. If setActive/ApiClient.setBaseUrl throws, it will surface as an unhandled rejection while the UI continues. Use a sync callback that explicitly handles the Promise (or enhance showPopup to handle async callbacks).

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +31
const stored = await ProfileStorage.getProfiles();
setData(stored);
setLoading(false);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refresh() doesn’t handle errors from AsyncStorage/JSON parsing (via ProfileStorage.getProfiles). If it throws, loading will stay true and screens using this hook will render nothing indefinitely. Add a try/catch to set an empty/default state and ensure setLoading(false) always runs (e.g., in a finally block).

Suggested change
const stored = await ProfileStorage.getProfiles();
setData(stored);
setLoading(false);
try {
const stored = await ProfileStorage.getProfiles();
setData(stored);
} catch (error) {
// Fallback to a safe default state if reading/parsing profiles fails
setData({
version: 1,
activeProfileId: null,
profiles: []
});
} finally {
setLoading(false);
}

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +92
const active = data.profiles.find((p) => p.id === data.activeProfileId);
return active?.serverUrl ?? data.profiles[0]?.serverUrl ?? null;
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If PROFILES storage contains profiles but activeProfileId is missing/invalid (e.g., partial/corrupted data), migrate() returns a URL but never normalizes/persists a valid activeProfileId. This leaves the app in a state where no profile is marked active in the UI. Consider defaulting activeProfileId to the first profile (and writing it back) when profiles exist but the active id is null/unknown.

Suggested change
const active = data.profiles.find((p) => p.id === data.activeProfileId);
return active?.serverUrl ?? data.profiles[0]?.serverUrl ?? null;
let active = data.profiles.find((p) => p.id === data.activeProfileId);
// If no valid active profile is set but profiles exist, default to the first
// profile and persist this choice so the UI has a consistent activeProfileId.
if (!active && data.profiles[0]) {
active = data.profiles[0];
const updatedData: ProfileStorageData = {
...data,
activeProfileId: active.id
};
await writeStorage(updatedData);
}
return active?.serverUrl ?? null;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants