Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b73c0c3
fix: resolve issue 581
Demian-Yushyn Jul 30, 2025
cc0e655
fix: update adjustments
Demian-Yushyn Jul 30, 2025
84a2156
fix: add timeout constant
Demian-Yushyn Jul 30, 2025
cd349b3
fix: adjust naming
Demian-Yushyn Jul 30, 2025
5aa8e55
fix: update connectivity
Demian-Yushyn Jul 30, 2025
64c59a1
fix: remove unused functions and hardcoded variables
Demian-Yushyn Jul 31, 2025
b4fcf31
fix: Removed unused functions and hardcoded variables
Demian-Yushyn Jul 31, 2025
253341d
Merge remote-tracking branch 'origin/fix/issue-581' into fix/issue-581
Demian-Yushyn Jul 31, 2025
6121c2a
fix: issue 581
Demian-Yushyn Jul 31, 2025
b26c3d7
fix: progress on issue 581
Demian-Yushyn Jul 31, 2025
f21a460
fix: apply updates
Demian-Yushyn Jul 31, 2025
14441a6
fix: update wi DI Config, thread fixes
Demian-Yushyn Jul 31, 2025
327bb30
fix: connectivity logic update
Demian-Yushyn Jul 31, 2025
a9074d0
fix: cacheValidity change
Demian-Yushyn Jul 31, 2025
dabc744
fix: unit tests update
Demian-Yushyn Aug 6, 2025
48d55bc
fix: removed team and coma
Demian-Yushyn Aug 6, 2025
786fa30
fix: removed extra comas
Demian-Yushyn Aug 6, 2025
a7475cd
Merge branch 'develop' into fix/issue-581
IvanStepanok Sep 16, 2025
0fce30b
fix: unit tests
Demian-Yushyn Sep 16, 2025
635c716
Merge branch 'develop' into fix/issue-581
Demian-Yushyn Oct 29, 2025
ad3f260
fix: removed state object warning
Demian-Yushyn Oct 29, 2025
39ace76
fix: update config
Demian-Yushyn Oct 29, 2025
8465734
Merge pull request #2 from raccoongang/fix/issue-581
IvanStepanok Nov 10, 2025
7196747
Merge branch 'openedx:develop' into develop
Demian-Yushyn Jan 28, 2026
b852394
Merge branch 'openedx:develop' into develop
Demian-Yushyn Feb 4, 2026
d702e00
Merge branch 'openedx:develop' into develop
Demian-Yushyn Mar 19, 2026
0e0a008
feat: update swiftlint config
IvanStepanok Apr 27, 2026
31e2c92
feat: add multi-tenant LMS Directory behind a feature flag
IvanStepanok Jul 6, 2026
fb69d14
fix: don't gate LMS directory on the app's stock-host reachability
IvanStepanok Jul 6, 2026
9ffc8cc
test: cover LMS directory offline handling; fix curated offline masking
IvanStepanok Jul 6, 2026
bdfd832
feat: brand sign-in for the selected LMS (logo, OAuth client, change)
IvanStepanok Jul 6, 2026
2e0b8fe
fix: theme fixes
IvanStepanok Jul 8, 2026
06c1a88
Merge remote-tracking branch 'upstream/develop' into feat/lms-directory
IvanStepanok Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ opt_in_rules: # some rules are only opt-in
excluded: # paths to ignore during linting. Takes precedence over `included`.
- Carthage
- DerivedData
- build
- Pods
- DerivedData
- Core/CoreTests
Expand Down
69 changes: 69 additions & 0 deletions AUDIT-20260706-2239.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
**1. Резюме**

Це не “красиве демо”, а великий реальний iOS-клієнт Open edX з живими API, кешем, курсами, профілем, дискусіями, прогресом, датами й downloads.
Але як готовий сервіс “під ключ” він зараз не доведений: дефолтний prod/stage конфіг веде на `localhost`, OAuth порожній, частина фіч вимкнена.
Найбільший розрив: проєкт залежить від конкретної Open edX інстанції та mobile API після грудня 2023, а в репозиторії немає доказу, що поточний build приймально пройдений на реальному LMS.
Multi-tenant/LMS Directory виглядає як нова надбудова, але runtime-override прапорці зберігаються і не всюди реально застосовуються.
Вердикт: сильна база для production app, але перед оцінювачами це ризикує виглядати як “майже готово”, якщо не закрити конфігурацію, інтеграційні докази й tenant edge cases.

**2. Інтернет-Контекст**

Публічний репозиторій визначає продукт як “modern vision” iOS-застосунку для Open edX від Raccoon Gang, а не окрему тендерну систему; README прямо вимагає налаштувати `config_settings.yaml` і `config.yaml` під свою Open edX конфігурацію. Там же сказано, що переклади не включені й мають підтягуватись окремо перед тестуванням/публікацією.

Проєкт таргетить актуальний Open edX release і mobile APIs; для платформ старіших за грудень 2023 потрібен API plugin. Це збігається з FC-0031: мета була підключити зміни edx-platform API для нових мобільних застосунків, які спирались на API-зміни, спершу розроблені Raccoon Gang. Plugin `mobile-api-extensions` прямо описаний як extended Open edX APIs for mobile applications.

Raccoon Gang є офіційним Open edX provider; їхній профіль виділяє custom LMS, mobile learning apps, gamification, e-commerce, theming, third-party auth, hosting/migration/support. Тобто оцінювачі очікуватимуть не просто екрани, а інтеграцію з реальним Open edX стеком.

Є Android-аналог з тією ж логікою: налаштувати config під Open edX, підтягнути translations, використовувати актуальні mobile APIs або plugin для старих платформ. iOS repo має latest release `v2.2(Ulmo.1)` від 23 жовтня 2025, тобто орієнтир очікувань зараз близький до сучасного Open edX mobile, а не legacy app.

**3. Критичні Розриви**

| Вимога / очікування | Що є насправді | Що треба |
|---|---|---|
| App має підключатись до реального Open edX LMS | `prod/config.yaml` має `API_HOST_URL: http://localhost:8000`, empty `OAUTH_CLIENT_ID`, LMS Directory off: [config.yaml](/Users/ivanstepanok/Developer/RaccoonGang/OPENEDX/openedx-app-ios/default_config/prod/config.yaml:1) | Зафіксувати приймальний config для конкретного LMS: host, OAuth client, SSO, privacy/TOS, Firebase, app store id |
| Сумісність з mobile APIs | Код реально ходить у `/api/mobile/v4`, course progress, dates, discussions, enrollment: [CourseEndpoint.swift](/Users/ivanstepanok/Developer/RaccoonGang/OPENEDX/openedx-app-ios/Course/Course/Data/Network/CourseEndpoint.swift:86) | Матриця “LMS release → supported / plugin needed”, smoke test на Redwood/Ulmo або клієнтській інстанції |
| Переклади перед публікацією | README каже, що translations не включені: [README.md](/Users/ivanstepanok/Developer/RaccoonGang/OPENEDX/openedx-app-ios/README.md:22) | Перед демо/прийомкою виконати translation pipeline або чесно обмежити мови |
| Multi-tenant override | Selection зберігає `courseUnitProgress`, `dashboardType`, `unknownUnitsMode`: [LMSOverridesStore.swift](/Users/ivanstepanok/Developer/RaccoonGang/OPENEDX/openedx-app-ios/Authorization/Authorization/Presentation/TenantPicker/LMSOverridesStore.swift:37), але `UIComponentsConfig` і `DashboardConfig` читають тільки plist: [UIComponentsConfig.swift](/Users/ivanstepanok/Developer/RaccoonGang/OPENEDX/openedx-app-ios/Core/Core/Configuration/Config/UIComponentsConfig.swift:26), [DashboardConfig.swift](/Users/ivanstepanok/Developer/RaccoonGang/OPENEDX/openedx-app-ios/Core/Core/Configuration/Config/DashboardConfig.swift:20) | Або застосувати overrides всюди, або прибрати ці поля з registry contract |
| LMS Directory demo data | DEBUG mock містить MIT/Stanford/Harvard/Oxford тощо з URL, які не є гарантовано Open edX API hosts: [lms_mock_data.json](/Users/ivanstepanok/Developer/RaccoonGang/OPENEDX/openedx-app-ios/Authorization/Authorization/Presentation/TenantPicker/lms_mock_data.json:1) | Для демо тільки валідний registry із реальними LMS, без впізнаваних фейкових платформ |
| Unknown/problem content | Unknown блоки відкриваються у WebView лише якщо `multiDevice == true`; registry `unknownUnitsMode` не використовується: [CourseUnitViewModel.swift](/Users/ivanstepanok/Developer/RaccoonGang/OPENEDX/openedx-app-ios/Course/Course/Presentation/Unit/CourseUnitViewModel.swift:18) | Політика fallback для unsupported XBlocks, proctored/timed exams, ORA, LTI та чітке повідомлення для learners |
| Надійність parsing | Course parsing має force unwrap для course/chapter/sequential/block: [CourseRepository.swift](/Users/ivanstepanok/Developer/RaccoonGang/OPENEDX/openedx-app-ios/Course/Course/Data/CourseRepository.swift:139) | Безпечний parser з graceful error на неповну/нестандартну відповідь LMS |
| Push/offline sync | FCM є, але dev configs мають `CLOUD_MESSAGING_ENABLED: false`: [config_single_tenant.yaml](/Users/ivanstepanok/Developer/RaccoonGang/OPENEDX/openedx-app-ios/default_config/dev/config_single_tenant.yaml:16); background task method є, але не викликається в launch flow: [AppDelegate.swift](/Users/ivanstepanok/Developer/RaccoonGang/OPENEDX/openedx-app-ios/OpenEdX/AppDelegate.swift:246) | Доказ end-to-end push token, notifications, offline progress sync, background behavior |
| Offline downloads | Реально є video/html downloads, але `problem` type декларований і майже не призначається: [DownloadManager.swift](/Users/ivanstepanok/Developer/RaccoonGang/OPENEDX/openedx-app-ios/Core/Core/Network/DownloadManager.swift:34) | Чітко визначити, що саме offline-supported: video, html, problem attempts, sync rules |

**4. Зайве**

LMS Directory / universal app, Report this LMS, runtime branding, tenant registry contract не описані в README як базова вимога. Вони можуть бути цінними, але зараз це найбільший шмат ризику.

Порожні `Course/Presentation/AIAssist` / `AIAssistant` директорії виглядають як незавершений слід, не функція.

App-level Dates і App-level Downloads є, але вимкнені дефолтно через config: [MainScreenView.swift](/Users/ivanstepanok/Developer/RaccoonGang/OPENEDX/openedx-app-ios/OpenEdX/View/MainScreenView.swift:129). Якщо це не входить у acceptance, не варто демонструвати як core scope.

Модераційний “Report this LMS” потребує registry backend `/api/v1/reports`: [ReportLMSView.swift](/Users/ivanstepanok/Developer/RaccoonGang/OPENEDX/openedx-app-ios/Profile/Profile/Presentation/Profile/ReportLMSView.swift:107). Без такого backend це зайва кнопка з ризиком провалу.

**5. Погляд Оцінювача**

Перші 10 хвилин: якщо build з дефолтним prod/stage config, оцінювач побачить застосунок, який не під’єднаний до production LMS. Це найгірше перше враження, бо README сам каже, що треба налаштувати Open edX config.

Якщо дати валідний LMS, core flow виглядає переконливо: login/register, discovery/enrollments, course home/content/progress/dates/offline/discussions/profile. Це близько до очікуваного Open edX mobile клієнта.

Що швидко помітять: unsupported content screen, неповні tenant overrides, вимкнений cloud messaging, localhost configs, фейковий LMS Directory catalog у DEBUG, відсутність видимого acceptance evidence.

Немає ознаки, що це окремий backend/dashboard сервіс. Це мобільний клієнт до Open edX; якщо клієнт очікував “мобілку з інтеграцією в їхню систему”, scope близький. Якщо очікував standalone SaaS із власним backend/admin, цього тут немає.

Тести в repo є: 570 Swift files і 49 test Swift files у основних модулях; CI запускає unit tests через Fastlane. Я тести не запускав через read-only sandbox і обмеження на записи Xcode/cache.

**6. Пріоритет Дій**

1. Зафіксувати реальний acceptance config: LMS URL, OAuth client, SSO, agreements, Firebase, app id, feature flags.

2. Пройти end-to-end smoke на реальному Open edX: login, registration, discovery, enroll, dashboard, course content, progress, dates, discussion post/reply, profile edit, logout.

3. Закрити tenant override bug: `UIComponentsConfig`, `DashboardConfig`, `FeaturesConfig`, unknown units policy мають читати persisted LMS overrides або contract треба скоротити.

4. Прибрати/ізолювати fake LMS Directory mock з демо-збірок; демо має йти через реальний registry.

5. Зробити compatibility checklist для LMS release/plugin: особливо `/api/mobile/v4`, progress, dates, downloads, notifications.

6. Прибрати незавершені сліди й вимкнені “обіцянки”: AI folders, Report LMS без backend, claims про push/offline якщо вони не демонструються.

7. Додати приймальний артефакт: короткий test report зі збіркою, версією LMS, user credentials, пройденими сценаріями, відомими обмеженнями.
15 changes: 13 additions & 2 deletions AppDates/AppDatesTests/Generated/AppDatesMocks.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import ZipArchive

public final class ConfigProtocolMock: ConfigProtocol, @unchecked Sendable {
public init() { }
public init(baseURL: URL = URL(fileURLWithPath: ""), baseSSOURL: URL = URL(fileURLWithPath: ""), ssoFinishedURL: URL = URL(fileURLWithPath: ""), ssoButtonTitle: [String: Any] = [String: Any](), oAuthClientId: String = "", tokenType: TokenType, feedbackEmail: String = "", appStoreLink: String = "", faq: URL? = nil, platformName: String = "", agreement: AgreementConfig, firebase: FirebaseConfig, facebook: FacebookConfig, microsoft: MicrosoftConfig, google: GoogleConfig, appleSignIn: AppleSignInConfig, features: FeaturesConfig, theme: ThemeConfig, uiComponents: UIComponentsConfig, discovery: DiscoveryConfig, dashboard: DashboardConfig, braze: BrazeConfig, branch: BranchConfig, program: DiscoveryConfig, experimentalFeatures: ExperimentalFeaturesConfig, URIScheme: String = "") {
public init(baseURL: URL = URL(fileURLWithPath: ""), baseSSOURL: URL = URL(fileURLWithPath: ""), ssoFinishedURL: URL = URL(fileURLWithPath: ""), ssoButtonTitle: [String: Any] = [String: Any](), oAuthClientId: String = "", tokenType: TokenType, feedbackEmail: String = "", appStoreLink: String = "", faq: URL? = nil, platformName: String = "", agreement: AgreementConfig, firebase: FirebaseConfig, facebook: FacebookConfig, microsoft: MicrosoftConfig, google: GoogleConfig, appleSignIn: AppleSignInConfig, features: FeaturesConfig, theme: ThemeConfig, uiComponents: UIComponentsConfig, lmsDirectory: LMSDirectoryConfig, discovery: DiscoveryConfig, dashboard: DashboardConfig, braze: BrazeConfig, branch: BranchConfig, program: DiscoveryConfig, experimentalFeatures: ExperimentalFeaturesConfig, URIScheme: String = "") {
self.baseURL = baseURL
self.baseSSOURL = baseSSOURL
self.ssoFinishedURL = ssoFinishedURL
Expand All @@ -37,6 +37,7 @@ public final class ConfigProtocolMock: ConfigProtocol, @unchecked Sendable {
self._features = features
self._theme = theme
self._uiComponents = uiComponents
self._lmsDirectory = lmsDirectory
self._discovery = discovery
self._dashboard = dashboard
self._braze = braze
Expand Down Expand Up @@ -138,6 +139,12 @@ public final class ConfigProtocolMock: ConfigProtocol, @unchecked Sendable {
}


private var _lmsDirectory: LMSDirectoryConfig!
public var lmsDirectory: LMSDirectoryConfig {
get { return _lmsDirectory }
set { _lmsDirectory = newValue }
}

private var _uiComponents: UIComponentsConfig!
public var uiComponents: UIComponentsConfig {
get { return _uiComponents }
Expand Down Expand Up @@ -257,7 +264,7 @@ public final class CoreAnalyticsMock: CoreAnalytics {

public final class CoreStorageMock: CoreStorage, @unchecked Sendable {
public init() { }
public init(accessToken: String? = nil, refreshToken: String? = nil, pushToken: String? = nil, appleSignFullName: String? = nil, appleSignEmail: String? = nil, cookiesDate: Date? = nil, reviewLastShownVersion: String? = nil, lastReviewDate: Date? = nil, user: DataLayer.User? = nil, userSettings: UserSettings? = nil, resetAppSupportDirectoryUserData: Bool? = nil, useRelativeDates: Bool = false, lastUsedSocialAuth: String? = nil, latestAvailableAppVersion: String? = nil, updateAppRequired: Bool = false) {
public init(accessToken: String? = nil, refreshToken: String? = nil, pushToken: String? = nil, appleSignFullName: String? = nil, appleSignEmail: String? = nil, cookiesDate: Date? = nil, reviewLastShownVersion: String? = nil, lastReviewDate: Date? = nil, user: DataLayer.User? = nil, userSettings: UserSettings? = nil, resetAppSupportDirectoryUserData: Bool? = nil, useRelativeDates: Bool = false, lastUsedSocialAuth: String? = nil, latestAvailableAppVersion: String? = nil, updateAppRequired: Bool = false, selectedLMSBaseURL: String? = nil) {
self.accessToken = accessToken
self.refreshToken = refreshToken
self.pushToken = pushToken
Expand All @@ -273,6 +280,7 @@ public final class CoreStorageMock: CoreStorage, @unchecked Sendable {
self.lastUsedSocialAuth = lastUsedSocialAuth
self.latestAvailableAppVersion = latestAvailableAppVersion
self.updateAppRequired = updateAppRequired
self.selectedLMSBaseURL = selectedLMSBaseURL
}


Expand Down Expand Up @@ -321,6 +329,9 @@ public final class CoreStorageMock: CoreStorage, @unchecked Sendable {
public private(set) var updateAppRequiredSetCallCount = 0
public var updateAppRequired: Bool = false { didSet { updateAppRequiredSetCallCount += 1 } }

public private(set) var selectedLMSBaseURLSetCallCount = 0
public var selectedLMSBaseURL: String? = nil { didSet { selectedLMSBaseURLSetCallCount += 1 } }

private let clearState = MockoloMutex(MockoloHandlerState<Never, @Sendable () -> ()>())
public var clearCallCount: Int {
return clearState.withLock(\.callCount)
Expand Down
Loading
Loading