Overview › Learning & Course Progress

Learning & Course Progress

Area: 05 Learning & Course Progress · 9 items · Source: PADI Mobile Scope Alignment v-ceb35f4d Subject: the unified PADI app — Expo React Native. Every verdict below is about what that app can build, not about what today's apps do.

What this card covers, and what it does not

The area holds 11 items — 2 carried forward from web, 9 in the Q1 2027 Delta lane.

This card is the nine Q1 Delta itemsCRS-01, CRS-02, CRS-03, LRN-01, LRN-02, LRN-03, LRN-05, LRN-06, LRN-07.

MYP-04 (eLearning Progress Section) and MYP-12 (Next-course recommendations) are carried forward from web, analysed in full in Logged In Experience, and restated here in one line each so the area reads as one page. They must be counted once across the two sub-tasks, not twice.

LRN-04 does not exist. The IDs run 01, 02, 03, then 05 to 07. This is the third such discontinuity in the artefact, after AUTH-07 and CERT-04. Anyone reconciling counts should expect six LRN- items, not seven.

This area has no Post Q1 2027 lane at all — 11 items, 2 carried, 9 Q1, nothing deferred. It is the first delta area in this study that needs no companion document.

Not one of the nine items carries a requirement. No detail drawer, no Impact, no Effort. The artefact's drawer data covers only web-derived items, and all nine of these are mobile-delta. Everything below is written against a title and a flag.


The finding to lead with

This area is the best-supplied in the study — and its four records disagree with each other in four different places.

Unlike Dive Log (tickets but no code) or Pro (code but no tickets), Learning has all of it: shipped implementations on both platforms, a web platform, a dedicated Expo spike, and epic PAM-9 at fix version Q1 - Release 2027 carrying eleven stories. Feasibility is largely settled. What is not settled is what anyone has actually committed to.

1. The artefact says committed; its own flags say parked. The area blurb reads "Course progress and the catalogue are carried forward. The delta is native delivery, offline study and the Training-app absorption — all of it committed for Q1 2027." Yet all nine items carry PRK and not one carries Now. In the artefact's data model s:"now" is the release lane and d:"parking" is the chip that renders — so the prose asserts a commitment the item flags withhold. The same contradiction appeared in Dive Log and Pro; this is its third showing.

2. The checkout decision is in Q1; the work it implies is in Later. CRS-02 carries the IAP flag and sits in the Q1 Delta lane. The Jira story that would deliver it — PAM-72 In-app purchase (Club, eCard, course bundles) — is at fix version Later - Release 2027. Either CRS-02 is a decision-only item that produces a memo and no code, or the sequencing is wrong. This card reads it as decision-only, and says so, because the alternative is to price a build nobody has funded.

3. Two offline tiers exist; one is quietly unfunded. LRN-02 names the on-demand tier specifically. In Jira, PAM-34 (On-Demand Download Tier) is Q1, while PAM-33 (Always-Available Tier, Auto-Cached) has no fix version at all. PAM-33 describes what is being dropped: "outlines, quiz questions, knowledge-review text, and safety checklists… automatically downloaded when I enroll." The artefact and the backlog agree only because the second tier has fallen out of both. Worth stating plainly, because "offline eLearning" in a readout will be heard as both tiers.

4. One backlog story is describing something else entirely. PAM-32 is titled "eLearning Theory Access (from Training App)", which reads as courseware. Its user flow is not: "User opens the Learn section… selects a topic, such as Hand Signals or Knots… views the theory content for the selected topic, including images and descriptions." That is reference content, not SCORM courseware. Hand signals and knots are already tool cells in the shipped iOS app, and they belong to area 12 Dive Tools & Reference (PAM-164), not here. The artefact's "Training-app absorption" phrase and PAM-32 are describing two different pieces of work under one name.

A fifth, smaller: PAM-36 (Next-Course Recommendations) is Later, while its artefact counterpart MYP-12 sits in this area's carried-forward lane as W26.


How to read the evidence in this card

The unified app is a new client. It inherits no code from padiww/ios-padi-app or padiww/android-padi-app — those apps are being replaced. They appear here as proof that an endpoint answers a real client holding a diver's token, never as a head start.

Citations name repositories, never folders. Paths are relative to the repository root. The iOS checkout carries .claude/worktrees/ copies of its own tree; those are working copies and nothing here cites them.

Four mobile surfaces were checked, not two. Beyond the two PADI apps, diviac/travel-rn (PADI Adventures) and padiww/padi-aware-flutter were both searched for course, eLearning and SCORM content. Neither has any — zero matches in either. So unlike Engagement, where Adventures turned out to own most of the area, Learning is genuinely a two-surface area. The one place Adventures does bear on this card is CRS-02, and it bears on it as a warning rather than a resource.

Two evidence classes carry most of this card:

  • Mobile-proven — shipping in padiww/ios-padi-app and/or padiww/android-padi-app against PADI's real hosts.
  • POC-proven — rebuilt in Expo in axelerant-padi/mobile-poc under spike PAM-17, which is the single most relevant artefact for LRN-02 and LRN-03 because it answers the React Native question directly rather than by analogy.

Built — unified app does not apply anywhere in this area. PadiWW/padi.mobile.app has no learning feature code. What is there is explicitly disclaimed: src/features/courses/types.ts:1-6 reads "Deliberately small, and not the Learning feature's model. PAM-51 and the real Learning work own entitlements, progress and the SCORM package", and src/features/courses/api/courseCatalog.ts:1-19 says "the real Learning feature should expect to pick a different endpoint rather than inherit this one." A generic file-system port at src/platform/FileSystem.ts:1-23 names "downloaded eLearning bundles" as a future use and has no caller. Read that as intent, not progress.

The three-step test, and where it lands here

For the three CRS- items and LRN-01LRN-03, step 1 hits: a mobile app already calls the endpoint. For LRN-05, LRN-06 and LRN-07, step 1 misses and step 2 hits — a PADI web front end calls it. That distinction is the card's second structural finding, and it is not a minor one.

But step 2 is not the last word, and this card was revised because of it. The POC is a fourth record, and it sits above both: where axelerant-padi/mobile-poc has built a thing in Expo against PADI's real hosts, that outranks "a web app does it" and outranks "a native app does it", because it answers the only question this study is actually asking — can the unified app do this?

Re-reading the nine against the POC moved LRN-05 from Existing — web to POC-proven, split LRN-06 into a proven read and an open write, and confirmed LRN-03's commit path in React Native. LRN-07 moved for a different reason — not the POC, but reading the Pro portal's actual API call. A verdict of "web-only" often survives only until someone opens the file.


The boundary that shapes three of the nine items

LRN-05, LRN-06 and LRN-07 are not consumer features today. They are instructor features.

  • LRN-06 — the digital forms exist, and they are good: PadiWW/pro.padi.com registers vue-signature-pad globally (src/main.js:15,102), captures a touch signature in src/components/training/FingerSign.vue:1-49, and renders a full RSTC medical questionnaire (src/components/training/MedicalForm.vue:1-50) and a Non-Agency Disclosure / liability release (src/components/training/NonAgencyForm.vue:1-45). But these are reached from the instructor's course-management flow, where a student signs in the instructor's presence.
  • What a diver gets instead is a PDF. PadiWW/account.padi.com src/lang/translations/en-US.json:99 instructs a parent to "download and complete the RSTC Medical Statement" at apps.padi.com/scuba-diving/elearning/medical.aspx — print, sign on paper, bring to the instructor.
  • LRN-07eRecord and Performance are first-class, localized surfaces on PadiWW/pro.padi.com (src/components/training/PadiERecord.vue:1-60, src/components/training/PadiPerformance.vue:1-20, src/components/smp/AssignedCourse.ViewERecord.vue). The string "eRecord" appears nowhere in either mobile app.
  • LRN-05 — the student half exists on PadiWW/learning.padi.com, but the issuing half is PadiWW/pro.padi.com (src/api/smp/code.js:49-56, assign a code to a PADI ID and SKU).

Two of those three have since moved, and the boundary is why they moved differently. The POC built diver-facing code redemption (LRN-05) and diver-facing form reading (LRN-06), which shows the boundary is about audience and ownership, not about capability: the diver-side APIs were there all along, unused by any mobile client. What did not move is form authoring — nobody has built a diver signing a document, on any surface — and LRN-07's instructor endpoint, which remains an instructor's. Read the rest of this section as describing where the work sits today, with those two corrections applied.

So a third of this area is Pro-owned territory being pulled into a consumer app. That is not a port. A diver-facing, self-service, legally-sound form flow does not exist on any PADI surface today, and building one raises consent, minor-consent and liability questions that are not engineering questions. This connects directly to FAM-08 on PAM-156 ("Workflows specific to pros, dive shops and resorts"), which is the item holding area 04 open.

One trap inside LRN-07. The mobile apps do have something that looks like performance tracking — the training-dive logbook with skills and instructor verification, against logbook.global-prod.padi.com. It is a different system from the Pro eRecord: different backend, different data model, different owner. Treating them as one item would produce an estimate for neither.


CRS-01 — Course Catalogue & Listing Pages + Course Tile Enrichment

Flags: PRK · Status: Existing — mobile + web · Feasibility: buildable; the delta is unspecified

What the unified app has to deliver

A browsable course catalogue with listing pages, and "tile enrichment" — additional fields on each course tile.

What it can rely on

All three surfaces already run off one backend, and it is commercetools.

  • iOS, native. The .courseCatalog cell opens a native view controller, not a web view — padiww/ios-padi-app PADI/feature/tools/view/ToolsViewController.swift:180-181 pushes CoursesListViewController.
  • Android, native. padiww/android-padi-app app/src/main/java/com/duns/padiapp/compose/course/CoursesScreen.kt:60-165 is the Compose equivalent.
  • Both call the same endpoint. GET api/Product/Search on https://api.padi.com/c/commerce/padiww/ios-padi-app PADI/data/api/impl/CourseCatalogueServiceImpl.swift:144 and :217-223 (filtered by productType.id), host at PADI/component/Configuration.swift:117; Android at app/src/main/java/com/duns/padiapp/data/api/NewCourseApi.kt:9-18.
  • The web storefront queries the same commercetools projection. PadiWW/customer-padi packages/frontend/backend/commerce-commercetools/actionControllers/ProductController.ts:45-46 builds a ProductQuery against commercetools product-projection-search, with the listing UI at packages/frontend/frontend/components/padi-product/product-list/index.tsx.

Three clients, one product backend, already proven against a real token. This is as de-risked as a catalogue gets.

What it has to build

The listing itself is a rebuild in React Native, which is ordinary work against a known contract.

"Course Tile Enrichment" is the actual delta, and it has no definition. Which fields enrich the tile, where they come from, and whether commercetools already carries them are all unstated. If the enrichment fields live outside commercetools — ratings, availability, instructor proximity — this stops being a catalogue item and becomes an integration.

What the POC deliberately did not do

axelerant-padi/mobile-poc excluded the catalogue on purposeCONTEXT.md:94 lists "catalogue item (catalogue/enrollment is out of scope)" among the terms to avoid, and CONTEXT.md:235 states the design principle plainly: "Matching is server-side; the client never reads commercetools."

So this item has no POC evidence, and the verdict rests entirely on the shipped apps. That is still strong — mobile-proven twice over, plus the web storefront on the same projection — but it is worth knowing that the one artefact built on the target stack skipped it.

And the principle matters more than the omission. If the unified app is not meant to read commercetools directly, the catalogue call goes through a BFF. That moves who owns tile enrichment, and it makes the static-X-API-KEY-versus-user-token question moot, because neither credential would be on the device.

One thing this card has not checked: whether the commercetools product projection already carries the enrichment fields. If it does, SCOPE-35 is a design choice; if not, it is an integration. Nobody has opened the projection to find out.

What blocks it

Nothing technical. SCOPE-35 asks what enrichment means.

Note for the API inventory. PadiWW/padi.com and PadiWW/scuba-diving contain no course catalogue code — the first is a blog/marketing/account rebuild, the second legacy Drupal. The web catalogue lives in PadiWW/customer-padi. An inventory built by searching the obvious repository name would miss it.


CRS-02 — Optimized Checkout & Sign-Up Wall Decision

Flags: PRK IAP · Status: Blocked — commercial · Feasibility: the client share is small; the decision is not ours

What the unified app has to deliver

A checkout path for courses, and a decision about the sign-up wall — when an anonymous user is asked to register.

What it can rely on

Almost nothing, and that is deliberate on the current apps' part.

There is no in-app purchase in either PADI app. Searched across both mobile repositories:

  • iOS imports StoreKit in exactly one file, for the App Store review prompt — padiww/ios-padi-app PADI/data/repository/impl/RatingRepositoryImpl.swift:8, calling SKStoreReviewController.requestReview at :182. No SKProduct, no SKPayment, no SKPaymentQueue.
  • Android has no BillingClient, BillingFlowParams or Play Billing dependency — not in app/src/main, not in any Gradle file.
  • Neither app references Adyen, Stripe or Braintree, the providers the web storefront uses.

Courses are bought by leaving the app. iOS builds a padi.com/courses/{key} URL with UTM parameters at padiww/ios-padi-app PADI/feature/common/CourseDetailViewController.swift:33-43 and hands it to UIApplication.shared.open (PADI/utility/UIApplication+Extension.swift:23) — the system browser, outside the app. Android does the same with an Intent.ACTION_VIEW implicit intent — app/src/main/java/com/duns/padiapp/compose/course/CoursesNavigation.kt:69-73 into compose/extensions/ContextExtension.kt:57-64.

The same pattern covers eCard purchase, so this is the app-wide commerce stance, not a course-specific quirk.

But PADI already takes payment inside an app — and that precedent is the wrong one

diviac/travel-rn (PADI Adventures) has a full native checkout. Card details are entered in the app, tokenised to Stripe (app/screens/AddCreditCardScreen.js:53, app/services/Api.js:226-228stripe_source_id, returnUrl: 'https://travel.padi.com/'), and orders post to checkout/orders (app/screens/CheckoutScreen.js:597) with 3-D Secure confirmed through a web view (app/screens/ConfirmCreditCard.js:4,48,51) against checkout/payment-intent/:intent/status/. There are screens for payment methods, order calculation, amendment, cancellation and refunds.

So the sentence "PADI has no in-app payment capability" is false, and anyone who checks will find it false. The accurate statement is narrower and more useful:

PADI takes payment inside an app today, and it is permitted because of what that app sells.

Apple and Google exempt real-world goods and services from their in-app-purchase rules — a dive trip, a charter, a booking. Adventures sells exactly that, so Stripe is allowed and no commission is owed. Courses and eLearning are digital goods consumed inside the app, which sit squarely inside the rules.

This is the trap in CRS-02, and it is an easy one to fall into. "Adventures already takes card payments natively, so the unified app can do the same for courses" is a reasonable-sounding inference and it is wrong. The Stripe integration is not reusable for course checkout — not because of engineering, but because the same code selling a different thing changes its compliance position.

What Adventures does establish: PADI holds a Stripe merchant relationship, has a working tokenisation and 3-D Secure flow, and has already shipped a native payment UI in React Native. If the answer to PADI-29 is "pay the commission and sell courses in-app", none of that helps. If it is "keep courses link-out and sell trips in-app", the unified app will need both patterns side by side — which is itself a design question nobody has asked yet.

What it has to build

Read the current design for what it is: the existing apps avoid Apple's and Google's digital-goods rules entirely by never offering an in-app buy. A link-out to a browser is outside the IAP regime. The moment the unified app puts a native "buy this course" affordance on screen, those rules apply to it.

So CRS-02 is not a checkout-engineering item. It is the same question CERT-05 reached and the same one PADI-7 has been holding open since April: does PADI accept platform commission on digital goods, or does the app stay link-out-only? The client-side work behind either answer is small. The decision behind it is not.

And if PADI says yes — is it technically buildable on today's APIs?

Yes, and the path is shorter than "build checkout" suggests. The chain is: take payment → grant the entitlement → confirm it landed.

Step Status
Take payment StoreKit / Play Billing. Ordinary client work, no PADI dependency
Grant the entitlement Exists, and is proven from React NativePOST /codes/{code}/redeem (axelerant-padi/mobile-poc src/learning/codesSpike.ts, app/activation-spike.tsx) and POST {LEARNING_URI}/v2/courses/{packageId}/enroll (PadiWW/learning.padi.com src/store/modules/redemption/actions.js:162-176)
Confirm it landed The POC polls entitlements with backoff and measured the real async window, which the web only guesses at with refreshTimer: 30000
Order record commercetools order creation exists server-side — PadiWW/customer-padi packages/frontend/backend/commerce-commercetools/actionControllers/CartController.ts:333
Receipt validation Missing. Nothing in the estate turns an Apple or Google receipt into a PADI entitlement

So the only genuinely new backend piece is receipt validation — a server that verifies a store receipt and calls the enrolment path that already exists. That belongs in PADI's API or Axelerant's BFF, and it is a well-understood component.

State this plainly at readout, because the usual shorthand overstates it. "We would have to build checkout" is wrong: the entitlement-granting half is built and proven on the target stack. CRS-02 is blocked commercially, not technically.

What blocks it

PADI-29 — the commission decision. And the sequencing problem in The finding to lead with: PAM-72 is at Later - Release 2027, so the implementation this item implies is not funded for Q1 even though the item sits in the Q1 lane. This card reads CRS-02 as decision-only for Q1.


CRS-03 — PADI Scuba Diver On-Ramp Entry Point

Flags: PRK · Status: Existing — mobile + web · POC-proven (navigation) · Feasibility: buildable; session binding unproven, and confirm which product is meant

What the unified app has to deliver

An entry point that brings a newcomer into the PADI Scuba Diver pathway.

What it can rely on

A complete, cross-surface flow already exists for Discover Scuba Diving, sharing one backend.

  • Deep-link entry, iOS: padiww/ios-padi-app PADI/data/repository/impl/FirebaseDynamicLinkRespositoryImpl.swift:142 handles deeplink=dsd, and :216-230 parses storeNumber and memberNumber, caches UTM in the Keychain and starts the flow.
  • Native affiliation API: PADI/data/api/impl/LearnAPIImpl.swift:20-30 calls GetCheckpointData / CreateCheckpoint / UpdateCheckPoint on the CheckPoint host, exposed at PADI/data/api/api/LearnAPI.swift:13-15.
  • In-app web view to https://learning.padi.com/dsdPADI/application/Constant/Constant.swift:194, opened at PADI/feature/learn/LearnViewController.swift:163-171. Note this is an in-app WKWebView, not the external browser used for purchase.
  • Android parity: app/src/main/java/com/duns/padiapp/PadiApplication.kt:149-171, target at domain/BaseAppSettingsIpm.kt:221-224, affiliation state persisted at :781-930.
  • The web calls the same CheckPoint API: PadiWW/learning.padi.com src/store/modules/checkpoint/actions.js:4-8, :20-24, :46-50, with the page at src/views/dsd/Dsd.vue.

One affiliation backend already serving a native client and a web client — the strongest possible starting point.

What it has to build

The app-side pieces are a deep-link handler, three CheckPoint calls and a web view. All three are proven twice.

What the POC proved, and what it explicitly did not

§2.8 of the eLearning spike reports DSD & My Promos as WORKING. src/learning/webLinks.ts performs a best-effort ssoExchangeTravel(idToken) hand-off, then pushes app/identity/webview.tsx to learning.padi.com/dsd?deeplink=dsd. The in-app portal hand-off is built and runs.

But the POC names its own limit, and it is the important half:

"The exchange mints a Travel SSO session; binding it to the learning portal is a real-app detail beyond the POC."

So navigation is proven and session binding is not. A diver reaching the DSD page unauthenticated is a different product from one arriving signed in, and the POC cannot tell you which the unified app will deliver. This is the same realm boundary that constrains LRN-05, LRN-06 and the Adventures preference model — it recurs because it is structural, not incidental.

What blocks it

A naming question, and it is not pedantic. SCOPE-36: the item says "PADI Scuba Diver On-Ramp". PADI Scuba Diver is a certification level; Discover Scuba Diving is a non-certification experience. Everything built today is DSD. If the item means DSD, it is nearly free. If it means a genuine Scuba Diver enrolment on-ramp, the affiliation plumbing transfers but the enrolment and payment path does not — and it lands straight back on CRS-02.


LRN-01 — Native in-app eLearning experience

Flags: PRK · Status: Existing — mobile · POC-proven for in-app delivery · Feasibility: the cheap reading is evidenced; the expensive one exists nowhere

What the unified app has to deliver

Course learning inside the app.

What it can rely on

Today there are two different eLearning experiences in the same app, and only one of them is native.

Online learning is a remote web view. Both platforms open PADI's own Vue web app:

  • iOS: padiww/ios-padi-app PADI/feature/learn/LearnViewController.swift:80 and :108 call webviewByLocation(type: .eLearning, isDetectSSO: true), resolving to https://learning.padi.com/training/dashboard (PADI/application/Constant/Constant.swift:195, host at PADI/component/Configuration.swift:44-45).
  • Android: app/src/main/java/com/duns/padiapp/compose/learn/LearnScreen.kt:94-114 and :236-256 open the same URL in WebInAppActivity.

Offline learning has a genuinely native shell. iOS carries a full UIKit screen tree under PADI/feature/offlineLearning/ — dashboard, course, asset list, download manager, SCORM reader, PDF and QuickLook readers. Android mirrors it in Compose at app/src/main/java/com/duns/padiapp/compose/elearning/ELearningNavigation.kt:18,26-105.

But the course content itself is still a web view in both. PADI/feature/offlineLearning/SCORM Reader/SCORMReader.swift:11-41 is a WKWebView pointed at locally extracted content through a custom URL-scheme handler; Android's PersistentWebView.kt does the same. The difference from online is that the content is local, not that it is native.

And the web has no native runtime either. PadiWW/learning.padi.com src/components/training/PadiCourse.vue:160-184 simply calls window.open on the content URL. Nothing at PADI renders course content without a web view — the mobile offline experience is already ahead of the web here.

And the POC delivered both halves in Expo

axelerant-padi/mobile-poc docs/elearning-poc-features.md carries a status line that answers Reading A directly:

MY COURSES: WORKING · COURSE DETAIL: WORKING · DOWNLOAD PAUSE/RESUME: WORKING · OFFLINE PLAYBACK: WORKING · ONLINE PLAYBACK: VALIDATED (non-prod) · PROGRESS/QUIZ COMMIT: WORKING

Online playback is the addition. §2.5 documents a two-phase WebView in src/learning/onlinePlayer.ts: mint CloudFront signed cookies by following a …/courseplayer?redirect=… 302, then load the player through buildOnlinePlayerUrl(). It runs — but it carries three constraints an estimate must price:

Constraint Consequence
Third-party cookies must be permitted in the WebView Platform-dependent; a future OS change is a live risk
mixedContentMode=compatibility Weakens the WebView's transport posture for that screen
A generic desktop-Chrome user agent, because the platform blocks device UAs The app has to lie about what it is to reach PADI's own content

The third one is not a workaround the study should quietly accept. The learning platform actively rejects mobile user agents, which is a platform decision PADI can change — and it is much cheaper to change than to work around forever. That is SCOPE-37's neighbour and it belongs in the same conversation.

And "VALIDATED (non-prod)" is not "WORKING". The online path was exercised against a non-production environment; the offline path was exercised end to end. Do not let a readout flatten the two.

What it has to build

That depends on a question this card will not answer for PADI.

SCOPE-37 — what does "native" mean in LRN-01?

Reading A: in-app rather than link-out. Already true for offline learning on both platforms, and the POC re-proved it in Expo. Modest work.

Reading B: natively rendered rather than web view. SCORM is an HTML-and-JavaScript packaging standard; rendering it without a web view means either replacing SCORM as the content format or writing a native runtime for it. This exists nowhere at PADI, including the web.

The two readings differ by an order of magnitude. No estimate for this item means anything until it is settled.

What blocks it

SCOPE-37. And a scoping correction: PAM-32 is not this item — see The finding to lead with.


LRN-02 — Offline eLearning download (on-demand tier)

Flags: PRK · Status: Existing — mobile · POC-proven · Feasibility: buildable, with one named unknown

What the unified app has to deliver

On-demand download of course content for offline study.

What it can rely on

A complete, shipping implementation on both platforms.

  • iOS: background download with progress notification, zip extraction, disk-size tracking and re-download of expired assets — padiww/ios-padi-app PADI/data/api/impl/AssetsApiImpl.swift:32-290, with Zip and ZIPFoundation vendored at Podfile:28,32, and a management UI at PADI/feature/offlineLearning/ManageDownloads/.
  • Android: app/src/main/java/com/duns/padiapp/compose/elearning/DownloadManagerViewModel.kt:1-121 tracks downloaded courses and deletes them recursively; ScormRoute.kt:173-192 reads imsmanifest.xml out of the unzipped package, confirming the same SCORM-zip approach.
  • The POC re-proved it in React Native. axelerant-padi/mobile-poc under spike PAM-17 implements download, local package management, manifest parsing and playback — src/learning/scorm/{download,downloadState,localPackages,manifest}.ts — and records pause/resume WORKING.

This is not a web-inherited capability: PadiWW/learning.padi.com has no offline store at all, only a browser download.

What it has to build

A port, not an invention — and the POC has already de-risked the React Native specifics.

What is honestly not proven

Storage eviction. The POC states it plainly: EVICTION POLICY: NOT IMPLEMENTED (docs/elearning-poc-features.md:304-310). Course packages are large, devices fill up, and no shipped platform here has a policy for what to drop when they do. That is real work nobody has done.

Quiz state is PARTIAL. §4.3 of the same document rates quiz-state persistence PARTIAL — commits flow, but resuming a part-finished quiz across an app restart is not proven. For a course whose assessment is the gate, that is the half that matters.

So this item carries three named unknowns, not one — eviction, two-device merge (LRN-03), and quiz-state resume. Each was found by building the thing rather than reading about it, which is the strongest evidence class in this study and also the reason the list is longer than the shipped apps suggest.

And the second tier has quietly vanished. PAM-33 (Always-Available, auto-cached on enrolment) has no fix version. If a readout says "offline eLearning is in Q1", it will be heard as covering the auto-cached essentials — outlines, quiz questions, safety checklists — and it does not.

What blocks it

Nothing. SCOPE-38 asks for an eviction policy; SCOPE-39 asks whether dropping PAM-33 was intended.


LRN-03 — Offline eLearning progress sync & conflict handling

Flags: PRK DEP · Status: Buildable — POC-proven in React Native · Feasibility: the conflict policy is a decision, not a missing capability

What the unified app has to deliver

Progress that survives being offline, and a defined answer when two devices disagree.

What it can rely on

The sync half ships and works.

  • One endpoint, both platforms: POST /learning/lms/commit on api.padi.compadiww/ios-padi-app PADI/data/service/AutoSyncDataService.swift:101-103 with the host at PADI/component/Configuration.swift:74-75; padiww/android-padi-app app/src/main/java/com/duns/padiapp/data/api/PADIApi.kt:9-13, and hardcoded into the SCORM player config at ScormRoute.kt:248.
  • A durable queue with retry: iOS caches unsynced commit payloads in shared UserDefaults and replays them on reconnect, retrying every 30 seconds up to three times before surfacing a failure — AutoSyncDataService.swift:32-155. Android persists pending activities in Room (ActivityEntity.kt:1-13, ActivityDao.kt:15) and drains them from a WorkManager job with OS-managed backoff (SyncAllWorker.kt:14-40, SyncAllActivitiesUseCase.kt:8-42).
  • The POC re-proved the commit path in React Native, including the part that could not be carried over directly — see below.

What it has to build — the half that does not exist

There is no conflict handling anywhere, on any platform.

Android's @Insert(onConflict = OnConflictStrategy.REPLACE) looks like conflict handling and is not: it is a database primary-key rule, not a business policy about whose progress wins. iOS has no versioning or timestamp merge at all — it replays cached payloads in order.

The POC says so without hedging (docs/elearning-poc-features.md:312-322):

Two-device offline-progress merge — NOT PROVEN. "Conflict resolution is effectively last-writer-wins on drain order… KD7 has no server status at all… two devices genuinely diverge with no merge point."

So the item's title names two things; the first ships and the second has never been built. This mirrors LOG-03 exactly, where the outbox is real and the concurrency control is client-side only.

One React Native specific, already solved. The native apps intercept SCORM CMI commits with an iOS override.js shim and an Android scheme handler. react-native-webview has neither, so the POC re-architected the commit path as injected JavaScript (src/learning/scorm/playerInjection.ts). That work is done and proven — it is not a fresh risk, but it is also not free to re-derive, and anyone estimating LRN-03 from the native apps alone will miss it.

Why this reads as buildable and not as a gap

The earlier verdict — Existing (sync) · Gap (conflict) — measured the item against the shipped apps, where conflict handling is genuinely absent. That is the wrong frame for this card.

We are not adding conflict handling to padiww/ios-padi-app. We are building a new unified app, and in a new app a conflict policy is something you implement, not something you inherit. Every ingredient is present:

  • the commit endpoint ships and is proven from React Native (playerInjection.ts)
  • a durable outbox pattern ships twice natively and once in the POC
  • PLT-01 funds the shared offline foundation this would live in

What is missing is the rule — and a rule is a decision, not a capability. Last-writer-wins is free, because it is what the drain order already does. Highest-progress-wins costs a comparison. Per-lesson merge costs a data-model change on both sides.

So: buildable, with the cost set by an answer PADI has not yet given. The honest risk is not "can we build it" but "we may build the cheap one and discover the product needed the expensive one."

What blocks it

A decision, not an API. SCOPE-40: what is the conflict policy for learning progress — last-writer-wins, highest-progress-wins, or per-lesson merge? The three have materially different costs and only the first is free.


What this card owes PLT-01

PLT-01 ("Shared offline foundation — save + sync + conflict handling") is one of the few items flagged Now. This card makes it the third area feeding that foundation, and the second wanting writes.

Area What it consumes Source
Certifications The read half only — pull-only cache, no outbox, no conflict policy 12-certifications-ecard.md
Dive Log The write half — durable outbox, five-state machine, conflict UI Dive Log
Learning The write half, with a different shape this card

The shapes differ, and that matters. A dive log is a record the diver authors and may edit; conflict means two versions of a document. Learning progress is a monotonic-ish stream of SCORM commits where conflict means two devices advancing through the same course. An outbox designed for one does not automatically serve the other — dive-log conflict wants a merge UI, learning conflict most likely wants a rule.

PAM-51 names all three in one story — "Offline-First Architecture (Dive Log + eCard + eLearning)" — which is the right instinct. But a foundation specified from the dive-log side alone will not fit this area, and the warning 12-certifications-ecard.md already carries applies here in reverse.


LRN-05 — Course access / activation code

Flags: PRK · Status: POC-proven · Feasibility: built natively and measured, not merely reachable

What the unified app has to deliver

Redeeming a course access or activation code.

What it can rely on

Step 1 misses and step 2 hits cleanly. Neither mobile app implements redemption — searched for activation code, access code, redeem, voucher, coupon, product code and PIC across both repositories. The only mobile artefact is a link-out: https://learning.padi.com/training/my-promos, opened as a web view from padiww/android-padi-app app/src/main/java/com/duns/padiapp/domain/AppConstant.kt:5 and padiww/ios-padi-app PADI/application/Constant/Constant.swift:199 via PADI/feature/learn/LearnViewController.swift:134.

The web owns it, and the API set is completePadiWW/learning.padi.com src/store/modules/redemption/actions.js:

Call Endpoint Line
List unredeemed GET {PRO_CODES_URI}/unredeemed :4-6
Redeem POST {PRO_CODES_URI}/{accessCode}/redeem :75-77
Redeem specialty POST {PRO_CODES_URI}/{accessCode}/redeem/specialty?packageid= :119-124
Enrol directly POST {LEARNING_URI}/v2/courses/{packageId}/enroll :162-176
Share a course POST {PRO_CODES_URI}/{accessCode}/share :178-201

with the redemption UI at src/components/training/PadiDashboard.vue:342-381.

And the POC built it natively — this is the strongest evidence in the card

axelerant-padi/mobile-poc does not merely reach the endpoints; it ships a working activation screen:

  • app/activation-spike.tsx — the redemption UI in Expo
  • src/learning/codesSpike.tsGET /codes/unredeemed and POST /codes/{couponCode}/redeem, on the PADI gateway with a Cognito idToken
  • a test suite alongside both

Two things it established that reading the web code could not:

  1. Redemption is irreversible. A code, once redeemed, cannot be un-redeemed — so this is one of the few spikes that consumed real inventory to prove itself. Any test plan for the unified app needs a code supply, and that is a logistics dependency, not a coding one.
  2. The entitlement lands asynchronously, and the POC measured the window. It polls entitlements with backoff after a successful redeem, rather than assuming the grant is immediate. PadiWW/learning.padi.com only guesses at this — refreshTimer: 30000. A measured window beats a hardcoded one, and the POC's number should be carried into the real implementation rather than re-guessed.

This moves the item from Existing — web to POC-proven: built on the target stack, against the real gateway, with the failure mode understood.

What it has to build

A production screen around a redemption path already built and measured in React Native. Ordinary work, and the least risky item on this card.

What blocks it

Nothing technical — but note the issuing side lives on PadiWW/pro.padi.com (src/api/smp/code.js:49-56 assigns a code to a PADI ID and SKU). Only the redeeming half belongs in a consumer app. SCOPE-41 asks whether the app is expected to carry any of the issuing side.


LRN-06 — Forms & applications

Flags: PRK · Status: POC-proven (list + view) · Open (authoring) · Feasibility: reading a diver's signed forms is built; signing one is not

What the unified app has to deliver

Forms and applications — in practice the RSTC medical statement, liability releases and safe-diving acknowledgements.

What it can rely on

The capability is real and the components are good, but they are on the instructor's surface — PadiWW/pro.padi.com, as set out in The boundary that shapes three of the nine items above: src/main.js:15,102 (signature library), src/components/training/FingerSign.vue:1-49 (capture), MedicalForm.vue:1-50 (RSTC questionnaire), NonAgencyForm.vue:1-45 (liability release), views/training/MedicalPhysician.vue (physician sign-off).

Nothing diver-facing exists. PadiWW/account.padi.com sends the diver to a static PDF (src/lang/translations/en-US.json:99). Mobile has only static terms-and-conditions prose — padiww/android-padi-app app/src/main/res/values/strings.xml:1075 describes the medical statement and the youth acknowledgement as things to read, with no form widgets, no signature capture and no submission.

The POC built the diver-facing read, and that splits this item cleanly in two

The claim above — "nothing diver-facing exists" — was true of the shipped apps and is no longer true of the estate. axelerant-padi/mobile-poc ships app/manage-forms.tsx and app/form-view.tsx: a diver's own forms, listed and opened inside the app.

Half Status Evidence
List a diver's forms POC-proven POST /forms/graphql as the primary source, with TrueVault as a secondary (TRUEVAULT_BASE: https://truevault-stage.global-np.padi.com/v1), querying /Search/Diver and /Search/Member
View an executed form POC-proven The PDF is fetched natively with the Bearer token and rendered in-app — not handed to an unauthenticated web view, which is what makes it a real in-app document rather than a link-out
Sign a new form Open Nothing built anywhere diver-facing; the Pro components prove the UI only

Two of these three matter more than they look. Fetching the PDF natively with the token is the detail that separates "we show you your form" from "we send you to a page that may ask you to log in again"; the POC did the harder one. And the two sourcesforms/graphql plus TrueVault — are a finding in their own right: a diver's executed forms live in more than one system, and which is authoritative is not documented anywhere this study has found. That belongs with PADI-30.

So the verdict splits: reading a diver's signed forms is POC-proven; authoring one is open — and it is the authoring half, not the reading half, that is blocked on legal.

What it has to build

Everything that makes signing a consumer flow: identity binding for the signature, a minor-consent path where a parent signs, storage and retrieval of an executed document, and whatever evidentiary standard PADI's legal position requires. The Pro components prove the UI is solvable; they do not prove the surrounding model exists.

What blocks it

Nothing blocks the reading half — it is built. What is blocked is authoring: PADI-30 — who owns a diver-signed form, and what makes a signature captured in a consumer app legally sufficient for a medical declaration. PADI-30 should also absorb the second question the POC raised: forms/graphql and TrueVault both return a diver's forms, and which is authoritative is undocumented. This is a legal question with an engineering consequence, in the same family as AUTH-11's parent-signed waivers, which is already blocked on PADI Legal. These two should be answered together, not separately.


LRN-07 — eRecord / performance

Flags: PRK · Status: Buildable — same API as the Pro portal · Feasibility: a plain REST read on the learning host; the write side is not a consumer action

What the unified app has to deliver

The eRecord — a student's record of skill performance for a course — and performance views.

What it can rely on

PadiWW/pro.padi.com again: src/components/training/PadiERecord.vue:1-60 renders the full eRecord (participant, PADI ID, registration code, dive-centre affiliation), PadiPerformance.vue:1-20 the performance dashboard with knowledge-review charts, and src/components/smp/AssignedCourse.ViewERecord.vue the instructor's view of a given student. "eRecord" is a localized, first-class term across that repository's translation files.

And the knowledge-development half already flows from mobile. SCORM completion syncs from both apps to api.padi.com/learning/lms/commit (see LRN-03), which is what populates the knowledge side of an eRecord. So the app is already a producer of part of this record without ever displaying it.

The exact API, and why buildable is the right word

The Pro portal's eRecord is not rendered from a bespoke internal service. PadiWW/pro.padi.com src/store/modules/course/actions.js:48-70 shows the data source in full:

const getCourseRecord = async (context, payload) => {
  const { customCourseId, affiliateId, authorization, platform } = payload;
  if (affiliateId) {
    url = new URL(`${VUE_APP_LEARNING_URI}/v2/courses/${customCourseId}/instructor/assessmentresults`);
    params.append('affiliateId', affiliateId);
  } else {
    url = new URL(`${VUE_APP_LEARNING_URI}/v2/courses/${customCourseId}/assessmentresults`);
    params.append('culture', culture);
  }

Two endpoints, one per audience, and the difference is the whole item:

Caller Endpoint Audience
With affiliateId GET /v2/courses/{customCourseId}/instructor/assessmentresults The instructor's view of a student
Without GET /v2/courses/{customCourseId}/assessmentresults The learner's own record

The second one is exactly what a diver-facing eRecord needs, it is a plain REST read on the learning host the app already talks to, and it is live in production today answering pro.padi.com. There is no new backend, no new realm and no new auth story — the unified app holds the same kind of token the portal does.

That is what moves this from Existing — web to Buildable. The distinction matters: Existing — web invites the reading "someone else's feature, ported at unknown cost", and this is not that. It is a documented endpoint with a known response shape and a working consumer to copy.

And it is the same endpoint MYP-04 needs. The MyPADI Personalised Home — API Reference documents /assessmentresults returning AssessmentResult{sectionId, title, isCompleted} — per-section booleans, from which a completion percentage is derivable. So LRN-07 and MYP-04 are two renderings of one call, and building either one gets most of the other. They should be sized together.

What it has to build

A read-only diver-facing view is the tractable scope, and it is genuinely useful: a diver seeing their own record. The write side — an instructor attesting that a skill was performed — is an instructor action and does not belong in a consumer app.

What blocks it

SCOPE-42: is LRN-07 the diver's read-only view, or the instructor's authoring surface? And the trap named above: the mobile logbook's skills and instructor verification are a different system (logbook.global-prod.padi.com) from the Pro eRecord. Conflating them yields an estimate for neither.


The two carried-forward items, in one line each

  • MYP-04 eLearning Progress SectionW26, analysed in full in Logged In Experience. Verdict: Gap, not buildable as specified. See the section below: the outstanding check on it is now closed.
  • MYP-12 Next-course recommendations (personalized)W26, analysed in full in Logged In Experience. Verdict: Existing — web, buildable via GET /v2/courses?tag=recommended; "personalized" remains undefined. Note PAM-36 sits at Later - Release 2027.

A check this card closes, and a narrower one it opens

Logged In Experience has carried an open item since Section 1: the response body of GET /v2/courses/{packageId}/details had never been read, and "if a percentage exists anywhere, it is there." It has now been read.

First, the endpoint is not what the name suggests. It is not the catalogue endpoint — that is commercetools Product/Search (see CRS-01). This is the LMS course-detail call: GET https://learning-prod.padi.com/learning/v2/courses/{courseId}/details, built at padiww/ios-padi-app PADI/data/api/impl/LanguageCourses/LanguageCoursesApiImpl.swift:26 with the host at PADI/component/Configuration.swift:118.

Its response model carries no progress. PADI/model/view/OfflineLearning/LanguageCourse.swift:10-29 decodes exactly: packageId, hasAnalytics, heroUrl, title, isSampleCourse, dateStarted, status, template, hasLegacy, availableLanguages[]. No completion percentage. No last-accessed datedateStarted is when a course was begun, not when it was last opened.

So MYP-04's gap holds as specified — but "proven by field list" was too strong. A Swift Decodable declares only the fields the client uses, so this proves the iOS app does not consume a percentage from that endpoint, not that the server omits one.

A third source has since arrived and it changes the shape of the answer. The MyPADI Personalised Home — API Reference documents GET /learning/v2/courses/{customCourseId}/assessmentresults returning AssessmentResult{sectionId, title, isCompleted}per-section booleans, from which a completion percentage is derivable. That is SCOPE-4's reduced form with an endpoint behind it. It also asserts CourseDetails.progressPercent, while declaring its own response shapes inferred rather than confirmed.

And it is not an unproven endpoint. An earlier draft of this card said no client had ever called /assessmentresults. That was wrong. PadiWW/pro.padi.com src/store/modules/course/actions.js:48-70 calls it in production to render the eRecord — see LRN-07. The accurate, narrower statement is: no mobile client calls it, and neither does this study's POC or the unified app. That is a much weaker objection, because a live web consumer proves the endpoint exists, is reachable, and returns a usable shape.

What remains genuinely unverified is the response body — the MyPADI reference declares its shapes inferred rather than confirmed, and nobody on this study has made the call. See Logged In Experience (MYP-04, target state) and PADI-31.

But a second model changes the next question. PADI/model/view/OfflineLearning/UserCourse.swift:25 — the offline-learning course payload returned by CultureCourseApi and CourseApi — declares percentRemaining: String?, alongside complete: Int?. Two things about it:

  • Nothing in the app reads either field. Zero call sites for percentRemaining outside its declaration.
  • Android declares no equivalent at all.

A field is declared in a Codable model because someone once saw it in a response. That makes this a hypothesis worth one live call, not a finding: does the server populate percentRemaining on the offline-learning course endpoint, and why does only iOS know about it? If it is populated, MYP-04's reduced form gets easier and the three-state badge may be a floor rather than a ceiling. If it is not, the gap is closed for good.

That is PADI-31, and it replaces the broader check. It is cheaper to answer than the question it retires.


Summary

Item Status Feasibility
CRS-01 Course catalogue + tile enrichment Existing — mobile + web Buildable; "enrichment" undefined, and the POC excluded the catalogue by design
CRS-02 Optimized checkout & sign-up wall Blocked — commercial · technically short The grant path exists and is POC-proven; only receipt validation is missing
CRS-03 PADI Scuba Diver on-ramp Existing — mobile + web · POC-proven (navigation) Buildable; session binding unproven, and confirm which product is meant
LRN-01 Native in-app eLearning Existing — mobile · POC-proven for in-app delivery The cheap reading is evidenced; the expensive one exists nowhere
LRN-02 Offline download (on-demand) Existing — mobile · POC-proven Buildable; three named limits — eviction, two-device merge, quiz-state resume
LRN-03 Offline progress sync & conflict Buildable — POC-proven in React Native The conflict policy is a decision, not a missing capability
LRN-05 Course access / activation code POC-proven Built natively and measured, not merely reachable — the least risky item here
LRN-06 Forms & applications POC-proven (list + view) · Open (authoring) Reading a diver's signed forms is built; signing one is legal-blocked
LRN-07 eRecord / performance Buildable — same API as the Pro portal A plain REST read on the learning host; the write side is not a consumer action

Counted as distinct capabilities: 9. No two of these are the same build.

Six of the nine now carry POC or direct-API evidence, which is a materially different picture from the first pass. The earlier draft measured every item against the two shipped native apps and the web; re-reading them against axelerant-padi/mobile-poc — an Expo app on the actual target stack — moved four verdicts and hardened two more. Where a thing has been built on the target stack, that is the evidence to cite, not the fact that a different app once did something similar.

Two cautions survive the upgrade:

  • LRN-06 and LRN-07 still reach into Pro-owned territory. The POC proved a diver can read their forms; it did not settle who owns the authoring surface, and LRN-07's instructor endpoint is a different audience on the same host.
  • POC-proven is not shipped. Eviction, two-device merge and quiz-state resume are named as unproven by the people who built it, which is why they are trustworthy — but they are still unbuilt.

What has to be answered

PADI owns these:

  1. PADI-29 — Does PADI accept platform commission on in-app digital goods, or does the app stay link-out-only? Blocks CRS-02. Same decision as PADI-7 and CERT-05; it should be answered once.
  2. PADI-30 — Who owns a diver-signed form, and what makes a signature captured in a consumer app legally sufficient? Blocks LRN-06. Answer alongside AUTH-11.
  3. PADI-31 — Does the server populate percentRemaining on the offline-learning course endpoint, and why does only iOS declare it? One live call. Refines MYP-04.

Scope owns these:

  1. SCOPE-35 — What is "Course Tile Enrichment"? Which fields, from which system?
  2. SCOPE-36 — Does CRS-03 mean Discover Scuba Diving, or a PADI Scuba Diver enrolment on-ramp?
  3. SCOPE-37 — What does "native" mean in LRN-01? In-app rather than link-out, or natively rendered rather than web view? No estimate is meaningful until this is settled.
  4. SCOPE-38 — What is the storage eviction policy for downloaded courses?
  5. SCOPE-39 — Was dropping the always-available tier (PAM-33, no fix version) intended?
  6. SCOPE-40 — What is the conflict policy for learning progress?
  7. SCOPE-41 — Does the app carry any of the access-code issuing side, or only redemption?
  8. SCOPE-42 — Is LRN-07 the diver's read-only view or the instructor's authoring surface?

And three that are bookkeeping, not engineering

  1. The area blurb says "all of it committed for Q1 2027" while all nine items are flagged PRK. One of the two is wrong and it should be corrected in the artefact rather than carried into planning.
  2. PAM-32 is filed under this epic but describes reference content — hand signals and knots — which is area 12 (PAM-164). It should be re-filed or re-titled.
  3. CRS-02 is Q1 while PAM-72 is Later, and MYP-12 is carried-forward while PAM-36 is Later. Two artefact-to-backlog lane mismatches in one area.