Feat/lms directory#664
Conversation
Fix/issue 581
Lets a single build browse the Open edX platforms published by a site registry, re-theme to the chosen one, and sign in against it. Off by default (LMS_DIRECTORY.ENABLED=false); stock single-tenant flow is untouched when the flag is off. - Core: LMSDirectoryConfig flag + ConfigProtocol.lmsDirectory; Config.baseURL resolves to the selected LMS when enabled; CoreStorage.selectedLMSBaseURL - Authorization: TenantPicker feature — directory search, QR login, per-LMS runtime theming, selection coordinator, remote + mock services - App: RouteController landing branch, AppDelegate flag-gated registration, LMSDirectoryRouter, NSCameraUsageDescription for QR - Profile: 'Report this LMS' flow (flag-gated) posting to the registry - default_config: LMS_DIRECTORY block for dev/stage/prod - Regenerated mocks for the new ConfigProtocol/CoreStorage members
The directory talks to the site registry, not the app's API host. It was being blocked by the global connectivity check (which pings API_HOST_URL) and by a ViewModel override that forced an offline state from the same signal — so in dev, where API_HOST_URL is an unreachable placeholder, the catalog never loaded even though the registry was reachable. Reachability is now decided by the actual registry call: a genuine URLError maps to .offline, an unreachable stock host does not. Removed the connectivity dependency from the directory service and view model.
- LMSDirectoryViewModel.loadFeatured now maps LMSDirectoryError.offline to the
.offline state (was swallowed into a generic .error), matching search.
- Add AuthorizationTests/…/LMSDirectory regression tests:
* RemoteLMSDirectoryService decodes registry items and maps real URLError
(notConnected / timedOut / cannotConnectToHost) to .offline — proving the
directory no longer depends on stock-host ConnectivityProtocol.
* LMSDirectoryViewModel constructs without any connectivity dependency and
surfaces .offline for both search and curated/featured loading.
Completes the per-LMS experience so a selected platform actually works and looks like itself: - Config.oAuthClientId / feedbackEmail honor the per-LMS values persisted at selection (was the cause of failed logins — the app sent the empty stock client id instead of the platform's registered mobile client). - SignInView shows the selected LMS logo (falls back to the app logo) and a 'Selected LMS / Change' banner returning to the directory landing. - LMSSelectionRouting.showLanding() + LMSDirectoryRouter implementation. - CoreTests: overrides apply when enabled+selected, ignored when the flag is off, and fall back to config when nothing is selected.
|
Thanks for the pull request, @IvanStepanok! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. 🔘 Update the status of your PRYour PR is currently marked as a draft. After completing the steps above, update its status by clicking "Ready for Review", or removing "WIP" from the title, as appropriate. Where can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
# Conflicts: # Theme/Theme/Theme.swift
Multi-tenant LMS Directory
https://openedx.atlassian.net/wiki/spaces/OEPM/pages/6532923408/Proposal+Multi-instance+configuration+for+the+Mobile+Apps
Lets a single build browse a directory of Open edX platforms, pick one, and sign in against it. When a learner selects a platform the app re-themes to that platform's accent color and branding, points the Core API at its host, and routes to its sign-in (or discovery). The whole feature sits behind the
LMS_DIRECTORYconfig flag, is disabled by default, and fails closed: when the flag is off or misconfigured the Config accessors fall back to the stock values and the app behaves exactly like today's single-tenant build.What's included
Directory & selection
LMSDirectoryLandingView). The intro screen offers "Find my LMS" and"Sign in with QR code"; from there yurface or a curated list.
DIRECTORY_MODEoverride (search/curated)wins, otherwise the registry's
GETsearchmode you get a debouncedwins, otherwise the registry'sGET /api/v1/configdecides. Insearch` mode you get a debounced(500ms) search box plus a recent-his there is no search box and a fixedfeatured list loads up front.
LMSDirectoryViewModelis a 7-sta,searching,results,empty,offline,error`) with explicit offline handling and cache fallback on selection.RemoteLMSDirectoryServicetalks search via?q=, featured via?featured=true),GET /api/v1/directory/{id}, andGET /api/v1/config. A DEBUG-onlyMockLMSDirectoryService(backed byitutes when no registry URL is set;in release builds the mock pathfatalErrors rather than reading the sample catalog, so no livemock-catalog logic runs in productio
LMSSelectionCoordinator.applySelection) persists the choice, applies thepalette, re-registers the API, then based on the platform's
preLoginDiscoveryflag.searchResultsShown,selectionMade,historyCleared.Branding & sign-in
Theme.Colors.update/Theme.UIColors.update. The accent veUIColor, so light/dark switching keeps working after re-theming. (Theme.Colors.updategains a few accent-driven parameters in thisPR — see the risk section.)
(
reRegisterAPI), so subsequent APId platform.SignInViewgets the full branded treatment: dynamic logo, a "Selected LMS" row with a "Change"button (routes back to the picker), ound
header. The logo and the"Selected LMS" row are gated onisDirectoryReachable`.SignUpViewandResetPasswordVieader image forLmsHeaderBackground()`.LmsHeaderBackgroundrenders the ckground URL when the persisted keyis set, and falls back to the bundledThemeAssets.headerBackgroundotherwise. When the feature isoff,
AppDelegateclears that key a back to the stock asset.QR sign-in
LMSDirectoryQRScannerViewwraps a hand-builtAVCaptureSessioncontroller (scans.qr/.aztec/.dataMatrix), with a dieticle, hint label, and close button. Presented as a sheet from both the intro and the search field.startRunning();NSCameraUsageDescriptionwas added toInfo.plist. A denied y (no live preview) rather than anerror path.Report this LMS
ReportLMSView)priate,scam,impersonation,spam,broken,other`), a required note, and an optional screenshot (downscaled to 1280px, JPEGq0.6, base64).
submitPOSTs{base_url, category, message, platform: "ios", app_version}(+ optionalreporter_email/screenshot_base6/reports. Reports go to the registry moderators, not to the LMS.i directory is not in curated mode (almsDirectory.isCurated` UserDefaults flag the view model writes when it loads the registry config).Institution/curated builds have no r
Configuration & feature flag
LMS_DIRECTORYblock parsed intoLMSDirectoryConfig(ENABLED,DIRECTORY_URL,DIRECTORY_MODE).isDirectoryReachable == enabled && !directoryURL.trimmed.isEmpty.Config.baseURL,baseSSOURL,oEmailconsult the persistedselection / UserDefaults overrides only whenisDirectoryReachable; otherwise they return the stockconfig values.
selectedLMSBaseURLviaCoreStorage/AppStorage, plusoverride keys for OAuth client id, fund, and accent colors).
AppStorage.clear()nilsselectedLMSBaseURLon logout.Connectivity & lifecycle
Connectivityreads `config.baseUead of freezing the host at init,because the active host can change at runtime. It records the probed URL alongside the 30s cache andonly honors the cache if it was prob with no fresh result for the current host it assumes reachable and lets the real request surface genuine failures. This avoids a
freshly-selected platform being wronunch-time default host is oftenlocalhost).
RouteController)er`) present the picker only when the feature is reachable and no platform is persisted; once one is picked, launch goes straight to thatplatform's sign-in.
observeLogout+AppStorage.clear()), so the picker returns on nextlaunch. A disabled or unreachable bued selection and resets branding atlaunch.
Tests
ConfigLMSDirectoryTests(Core): misconfiguration guard.RemoteLMSDirectoryServiceTests(Authorization): decoding and URLError →.offlinemapping via astub URL protocol.
LMSDirectoryViewModelTests(Authorization): search/offline/curated states and the QR selectionpath.
How to enable
Off by default. Set
ENABLED: truen the relevantconfig.yaml: