Skip to content

Feat/lms directory#664

Draft
IvanStepanok wants to merge 33 commits into
openedx:developfrom
raccoongang:feat/lms-directory
Draft

Feat/lms directory#664
IvanStepanok wants to merge 33 commits into
openedx:developfrom
raccoongang:feat/lms-directory

Conversation

@IvanStepanok

Copy link
Copy Markdown
Contributor

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_DIRECTORY config 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

  • Pre-login platform picker (LMSDirectoryLandingView). The intro screen offers "Find my LMS" and
    "Sign in with QR code"; from there yurface or a curated list.
  • Two modes resolved once at init: a client-side DIRECTORY_MODE override (search / curated)
    wins, otherwise the registry's GET searchmode 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.
  • LMSDirectoryViewModel is a 7-sta, searching, results, empty,offline, error`) with explicit offline handling and cache fallback on selection.
  • Recent history is persisted in a sre, capped at 10 entries,pinned-then-recent ordering.
  • RemoteLMSDirectoryService talks search via ?q=, featured via?featured=true), GET /api/v1/directory/{id}, and GET /api/v1/config. A DEBUG-only MockLMSDirectoryService (backed byitutes when no registry URL is set;in release builds the mock path fatalErrors rather than reading the sample catalog, so no live
    mock-catalog logic runs in productio
  • Selecting a platform (LMSSelectionCoordinator.applySelection) persists the choice, applies the
    palette, re-registers the API, then based on the platform'spreLoginDiscovery flag.
  • Four analytics events behind a no- searchResultsShown,selectionMade, historyCleared.

Branding & sign-in

  • Selection re-brands the running ap light/dark accent colors from theplatform's accent and reassigns Theme's global color slots via Theme.Colors.update /
    Theme.UIColors.update. The accent ve UIColor, so light/dark switching keeps working after re-theming. (Theme.Colors.update gains a few accent-driven parameters in this
    PR — see the risk section.)
  • The Core API is re-registered in the Swinject container scope with the tenant's host
    (reRegisterAPI), so subsequent APId platform.
  • SignInView gets the full branded treatment: dynamic logo, a "Selected LMS" row with a "Change"
    button (routes back to the picker), oundheader. The logo and the"Selected LMS" row are gated onisDirectoryReachable`.
  • SignUpView and ResetPasswordVieader image forLmsHeaderBackground()`.
  • LmsHeaderBackground renders the ckground URL when the persisted keyis set, and falls back to the bundled ThemeAssets.headerBackground otherwise. When the feature is
    off, AppDelegate clears that key a back to the stock asset.

QR sign-in

  • LMSDirectoryQRScannerView wraps a hand-built AVCaptureSession controller (scans .qr /
    .aztec / .dataMatrix), with a dieticle, hint label, and close button. Presented as a sheet from both the intro and the search field.
  • Scanned values are normalized to ahe registry search endpoint (exactcase-insensitive host match preferred, else first result), then applied through the same selection path as tapping a catalog result.
  • Camera permission is the implicit iOS prompt fired by startRunning(); NSCameraUsageDescription was added to Info.plist. A denied y (no live preview) rather than anerror path.

Report this LMS

  • New Profile flow (ReportLMSView)priate, scam, impersonation,spam, broken, other`), a required note, and an optional screenshot (downscaled to 1280px, JPEG
    q0.6, base64).
  • submit POSTs {base_url, category, message, platform: "ios", app_version} (+ optional
    reporter_email / screenshot_base6/reports. Reports go to the registry moderators, not to the LMS.
  • The entry point shows only when 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

  • New top-level LMS_DIRECTORY block parsed into LMSDirectoryConfig (ENABLED, DIRECTORY_URL, DIRECTORY_MODE).
  • Gate: isDirectoryReachable == enabled && !directoryURL.trimmed.isEmpty.
  • Config.baseURL, baseSSOURL, oEmail consult the persistedselection / UserDefaults overrides only when isDirectoryReachable; otherwise they return the stock
    config values.
  • Selection is persisted in UserDefaults (selectedLMSBaseURL via CoreStorage/AppStorage, plus
    override keys for OAuth client id, fund, and accent colors).AppStorage.clear() nils selectedLMSBaseURL on logout.

Connectivity & lifecycle

  • Connectivity reads `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 and
    only 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).
  • Launch routing (RouteController)er`) present the picker only when the feature is reachable and no platform is persisted; once one is picked, launch goes straight to that
    platform's sign-in.
  • Logout purges the selection (observeLogout + AppStorage.clear()), so the picker returns on next
    launch. A disabled or unreachable bued selection and resets branding atlaunch.

Tests

  • ConfigLMSDirectoryTests (Core): misconfiguration guard.
  • RemoteLMSDirectoryServiceTests (Authorization): decoding and URLError → .offline mapping via a
    stub URL protocol.
  • LMSDirectoryViewModelTests (Authorization): search/offline/curated states and the QR selection
    path.

How to enable

Off by default. Set ENABLED: true n the relevant config.yaml:

LMS_DIRECTORY:
  ENABLED: true
  DIRECTORY_URL: "https://your-registry.example.com"
  DIRECTORY_MODE: ""   # "" | "searc to the registry /api/v1/config
                                                                                                      With ENABLED: false or an empty DIREble is false and the app is stocksingle-tenant. As shipped, dev has the feature on (pointing at a demo registry) and prod / stage have it off.

Demian-Yushyn and others added 30 commits September 16, 2025 11:05
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.
@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Jul 8, 2026
@openedx-webhooks

Copy link
Copy Markdown

Thanks for the pull request, @IvanStepanok!

This repository is currently maintained by @openedx/openedx-mobile-maintainers.

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 approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To 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:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If 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 PR

Your 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:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@openedx-webhooks openedx-webhooks added the core contributor PR author is a Core Contributor (who may or may not have write access to this repo). label Jul 8, 2026
@github-project-automation github-project-automation Bot moved this to Needs Triage in Contributions Jul 8, 2026
@mphilbrick211 mphilbrick211 moved this from Needs Triage to Waiting on Author in Contributions Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core contributor PR author is a Core Contributor (who may or may not have write access to this repo). open-source-contribution PR author is not from Axim or 2U

Projects

Status: Waiting on Author

Development

Successfully merging this pull request may close these issues.

4 participants