# APWP-AO Dashboard Section — Phased Implementation Steps

Reference plan: `apwp-ao-dashboard-section-plan.md`

---

## Completion Log

| Phase | Status | Notes |
|---|---|---|
| Phase 1 | ✅ Complete | `userEverPurchasedSku()` added to `PluginAccessService` after `userHasAccessToSku()` |
| Phase 2 | ✅ Complete | `$aoActiveSlots`/`$aoPluginKey` removed; `$aoEverPurchased`, `$aoHasActiveAccess`, `$aoReactivateQty` added; `$aoLicenseCount` now returns 0 when lapsed |
| Phase 3 | ✅ Complete | See deviation note below |
| Phase 4 | ✅ Complete | See deviation notes below |
| Phase 5 | ✅ Complete | Static checks all pass. Browser automation unavailable (extension not connected) — interactive items require manual sign-off. See Phase 5 results below. |

### Phase 4 Deviations

**`@php` block placement** — Plan placed the `@php` data block inside the card and the `@if($aoEverPurchased)` gate only around the card's inner content. Implementation instead places the `@php` block *before* the `@if($aoEverPurchased)` gate (which now wraps the entire card `<div>`). This is required because the JS guards at the bottom of the file (`@if($aoHasActiveAccess && $aoActiveSlots->...)`) reference `$aoActiveSlots`, which would be undefined if the `@php` were inside a conditional that didn't run. Keeping the `@php` outside ensures variables are always defined regardless of `$aoEverPurchased`.

**Variable name prefix** — PHP variables in the dashboard `@php` block are prefixed `$aoUser` (instead of reusing `$user`) to avoid clobbering any `$user` variable that might be set elsewhere in the view by other sections.

**`$aoUser` null guard** — Added `$aoUser ? ... : false` and `$aoUser ? ... : collect()` guards in the `@php` block since the dashboard route doesn't abort on unauthenticated (unlike `DownloadController`). The middleware protects the route, but the guard makes the blade safe even if called without auth context.

**Connected Sites slot background** — Slot list items use `bg-gray-50 dark:bg-gray-900` (matching the dashboard card background) instead of `bg-white dark:bg-gray-800` used in the downloads page, so they visually nest correctly within the dashboard card.

**Dashboard message modal placement** — Modal markup is inserted immediately before the existing `<script>` block (outside any `@if`), matching the original downloads page placement. The JS guards ensure the modal JS only runs when there are active slots with a `domain_url`.

### Phase 3 Deviation

**Plan** described Steps 3.1, 3.2, and 3.3 as separate surgical removes/replacements. **Actual implementation** replaced the entire apwp-ao `<section>` block (Steps 3.1 + 3.3 combined) in one edit, then removed all bottom JS blocks (Step 3.2) in a second edit. Outcome is identical — cleaner to execute as two edits rather than four to avoid mismatched context from intermediate state.

**Structural note:** The blank line between the description `<div>` and the buttons `<div>` was removed as part of the section replacement (was only needed to separate the plugin key block). No functional impact.

---

## Phase 1 — Service Layer

**File:** `app/Services/PluginAccessService.php`

**Step 1.1** — Add `userEverPurchasedSku(User $user, string $sku): bool` after the existing `userHasAccessToSku()` method (around line 110).
- Mirror the order query from `purchasedLicenseQty()` (line 124)
- Use `->exists()` — no payment validity check
- Wrap in try/catch, return `false` on failure with `Log::warning`

**Verify:** Unit-testable in isolation — a user with a completed order for apwp-ao but an expired/cancelled payment should return `true`. A user with no order should return `false`.

---

## Phase 2 — DownloadController Updates

**File:** `app/Http/Controllers/DownloadController.php`

**Step 2.1** — Remove from the `index()` method:
- The `$aoActiveSlots` query (lines 47–50)
- The `$aoPluginKey` assignment (line 51)
- Both variables from the `compact()` call (line 53)

**Step 2.2** — Add to the `index()` method after the existing `$aoProduct` line:
```php
$aoEverPurchased   = PluginAccessService::userEverPurchasedSku($user, 'apwp-ao');
$aoHasActiveAccess = $aoEverPurchased && PluginAccessService::userHasAccessToSku($user, 'apwp-ao');
$aoOccupiedSlots   = PluginAccessService::aoUsedSlots($user);
$aoReactivateQty   = max(1, (int) ceil($aoOccupiedSlots / 5));
```

**Step 2.3** — Add `$aoEverPurchased`, `$aoHasActiveAccess`, and `$aoReactivateQty` to the `compact()` call. Retain `$aoProduct` and `$aoLicenseCount`.

**Verify:** Confirm the downloads view receives the correct variables (no PHP undefined variable errors on page load).

---

## Phase 3 — Downloads Page Cleanup & Button Update

**File:** `resources/views/account/downloads.blade.php`

**Step 3.1** — Remove from the apwp-ao `<section>`:
- Lines 79–103: the entire `@if($aoLicenseCount > 0)` plugin key block
- Lines 118–156: the "Connected Sites" block and surrounding `<div>`

**Step 3.2** — Remove from the bottom of the file:
- Lines 197–229: key show/hide/copy `<script>` and its `@if` guard
- Lines 231–388: dashboard message modal markup and all associated JS
- Lines 391–426: release slot `<script>` and its `@if` guard

**Step 3.3** — Replace the existing purchase button form (lines 107–115) with the three-way conditional:

```blade
@if($aoHasActiveAccess)
  <form method="POST" action="{{ route('cart.add') }}" class="js-add-to-cart">
    @csrf
    <input type="hidden" name="product_id" value="{{ $aoProduct->id }}">
    <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
      Purchase Additional Slots
    </button>
  </form>
@elseif($aoEverPurchased)
  <form method="POST" action="{{ route('cart.add') }}" class="js-add-to-cart">
    @csrf
    <input type="hidden" name="product_id" value="{{ $aoProduct->id }}">
    <input type="hidden" name="quantity" value="{{ $aoReactivateQty }}">
    <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
      Reactivate Subscription
    </button>
  </form>
@else
  @if($aoProduct)
    <form method="POST" action="{{ route('cart.add') }}" class="js-add-to-cart">
      @csrf
      <input type="hidden" name="product_id" value="{{ $aoProduct->id }}">
      <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
        Purchase License
      </button>
    </form>
  @else
    <a href="{{ route('products.index') }}" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
      Purchase License
    </a>
  @endif
@endif
```

**Verify:** Downloads page renders cleanly for all three user states. No plugin key field, no connected sites list. Slot count badge still visible when `$aoLicenseCount > 0`.

---

## Phase 4 — Dashboard Section

**File:** `resources/views/dashboard.blade.php`

**Step 4.1** — Add the `@php` data block at the opening of the new section (after the Gift Subscriptions card closing `</div></div>`):

```php
@php
    $user = auth()->user();
    $aoEverPurchased   = \App\Services\PluginAccessService::userEverPurchasedSku($user, 'apwp-ao');
    $aoHasActiveAccess = $aoEverPurchased && \App\Services\PluginAccessService::userHasAccessToSku($user, 'apwp-ao');
    $aoProduct         = \Vanilo\Product\Models\Product::where('sku', 'apwp-ao')->first();
    $aoPluginKey       = $user->ao_plugin_key ?? null;
    $aoLicenseCount    = $aoHasActiveAccess ? \App\Services\PluginAccessService::aoAllowedSlots($user) : 0;
    $aoActiveSlots     = $user->pluginAoDomains()
        ->where('is_active', true)
        ->orderBy('authorized_at')
        ->get(['id', 'domain_label', 'domain_url', 'authorized_at', 'last_connected_at']);
    $aoReactivateQty   = max(1, (int) ceil($aoActiveSlots->count() / 5));
@endphp
```

**Step 4.2** — Add the card markup. The section is only rendered when `$aoEverPurchased` is true. Inside, a `relative` wrapper contains:
- **Content layer** (`div`) — wraps the plugin key block and connected sites block; receives `opacity-40 pointer-events-none select-none` classes when `!$aoHasActiveAccess`
- **Plugin key block** — copied from the removed downloads section (lines 79–103); retains all existing classes and element IDs (`aamo-key-field`, `aamo-key-toggle`, `aamo-key-copy`)
- **Connected Sites block** — copied from the removed downloads section (lines 118–156); retains slot count badge, `js-ao-set-message` and `js-release-ao-slot` data attributes
- **"Purchase Additional Slots" button** — inside the content layer, below the sites list, `justify-end` aligned; qty defaults to 1 (no quantity input)
- **Reactivate overlay** (`div.absolute.inset-0`) — rendered only when `!$aoHasActiveAccess`; contains a `cart.add` form with `product_id` and `quantity={{ $aoReactivateQty }}`

**Step 4.3** — Move the dashboard message modal `<div>` markup (currently downloads.blade.php lines 232–262) to the bottom of `dashboard.blade.php`, outside any conditional, before the closing `</x-app-layout>`.

**Step 4.4** — Move and adapt the three JS blocks to the bottom of `dashboard.blade.php` with updated guards:

| Script | Guard |
|---|---|
| Plugin key show/hide/copy | `@if($aoHasActiveAccess && $aoPluginKey)` |
| Dashboard message modal + TinyMCE | `@if($aoHasActiveAccess && $aoActiveSlots->where('domain_url', '!=', null)->isNotEmpty())` |
| Release slot | `@if($aoHasActiveAccess && $aoActiveSlots->isNotEmpty())` |

**Verify:** Dashboard renders correctly for all three user states. Active subscriber sees interactive key + slots + "Purchase Additional Slots". Lapsed subscriber sees grayed content with "Reactivate Subscription" overlay. Never-purchased user sees no section.

---

## Phase 5 — End-to-End Verification

### Static Verification Results (completed)

| Check | Result |
|---|---|
| PHP syntax — `PluginAccessService.php` | ✅ No errors |
| PHP syntax — `DownloadController.php` | ✅ No errors |
| `@if`/`@endif` balance — `dashboard.blade.php` | ✅ 16 / 16 |
| `@if`/`@endif` balance — `downloads.blade.php` | ✅ 5 / 5 |
| `@foreach`/`@endforeach` balance — `dashboard.blade.php` | ✅ 2 / 2 |
| Stale `$aoActiveSlots` / `$aoPluginKey` refs in downloads view | ✅ None found |
| All 5 variables used in downloads view passed by controller | ✅ Confirmed |
| `userEverPurchasedSku()` defined in service | ✅ Line 117 |
| `userEverPurchasedSku()` called from DownloadController | ✅ Line 46 |
| `userEverPurchasedSku()` called from dashboard `@php` | ✅ Line 172 |
| Dashboard `@php` block before `@if($aoEverPurchased)` gate | ✅ Lines 170–181 |
| `quantity` hidden input on dashboard reactivate form | ✅ Line 276 |
| `quantity` hidden input on downloads lapsed form | ✅ Line 90 |
| "Purchase Additional Slots" in dashboard (active state) | ✅ Line 258 |
| "Purchase Additional Slots" in downloads (active branch) | ✅ Line 84 |
| "Reactivate Subscription" in dashboard overlay | ✅ Line 278 |
| "Reactivate Subscription" in downloads lapsed branch | ✅ Line 91 |
| "Purchase License" in downloads never-purchased branch | ✅ Lines 98 / 101 |
| Dashboard JS guards use `$aoHasActiveAccess` (not old guards) | ✅ Lines 361, 395, 520 |
| Qty edge cases: 0→1, 3→1, 5→1, 6→2, 11→3 | ✅ Confirmed via PHP |
| `$aoOccupiedSlots` local to controller (not leaked to view) | ✅ Confirmed |

### Manual Verification Required

Browser extension was not connected during Phase 5. The following items require sign-off in the running app (Sail is up at `localhost:80`):

| # | Item | How to test |
|---|---|---|
| 1 | Active subscriber — dashboard section renders with plugin key, slot rows, "Purchase Additional Slots" | Log in as user with active apwp-ao payment; visit `/dashboard` |
| 2 | Active subscriber — plugin key show/hide/copy works | Click Show, Copy on dashboard section |
| 3 | Active subscriber — downloads shows "Purchase Additional Slots" only (no key, no slots) | Visit `/account/downloads` |
| 4 | Lapsed subscriber — dashboard shows grayed section with "Reactivate Subscription" overlay centred | Log in as user with expired/cancelled payment; visit `/dashboard` |
| 5 | Lapsed subscriber — "Reactivate Subscription" adds correct qty to cart | Click button; check cart qty matches `ceil(occupied_slots/5)` |
| 6 | Lapsed subscriber — downloads shows "Reactivate Subscription" with correct qty | Visit `/account/downloads`; click button; check cart |
| 7 | Never purchased — no APWP-AO section on dashboard | Log in as user with no apwp-ao order; visit `/dashboard` |
| 8 | Never purchased — downloads shows "Purchase License" | Visit `/account/downloads` |
| 9 | Slot release from dashboard fires DELETE and reloads | **Note:** Release triggers a `confirm()` dialog — dismiss manually first |
| 10 | Set Message modal opens, loads message, saves | Click Set Message on a slot row with `domain_url` set |

### Phase 5 Bug Fix — Gift subscriber section not visible

**Reported:** User with an active gift subscription for apwp-ao saw no dashboard section.

**Root cause:** `$aoHasActiveAccess` was computed as `$aoEverPurchased && userHasAccessToSku()`. The short-circuit on `$aoEverPurchased` meant gift-only users (no completed order) always got `false`, and the `@if($aoEverPurchased)` gate then hid the section entirely.

**Fix applied to `dashboard.blade.php` `@php` block:**
- `$aoHasActiveAccess` now calls `userHasAccessToSku()` directly (checks both orders and gifts)
- Added `$aoSectionVisible = $aoEverPurchased || $aoHasActiveAccess`
- Section gate changed from `@if($aoEverPurchased)` → `@if($aoSectionVisible)`

**Fix applied to `DownloadController.php`:**
- `$aoHasActiveAccess` decoupled from `$aoEverPurchased` — same pattern

**Resulting state logic:**

| User state | `$aoEverPurchased` | `$aoHasActiveAccess` | `$aoSectionVisible` | Section display |
|---|---|---|---|---|
| Active purchase | true | true | true | Full, active |
| Lapsed purchase | true | false | true | Grayed + Reactivate overlay |
| Active gift only | false | true | true | Full, active |
| Expired gift only | false | false | false | Hidden (never purchased, nothing to reactivate) |
| Never | false | false | false | Hidden |

The downloads page three-way button logic (`$aoHasActiveAccess` / `$aoEverPurchased` / else) was already correct once the short-circuit was removed — no change needed there.

### Phase 5 Note (original checklist)

Work through the full verification checklist:

1. **Active subscriber — Dashboard** — Plugin key show/hide/copy works. Slot rows visible with Set Message and Release buttons. "Purchase Additional Slots" button adds qty 1 to cart.
2. **Active subscriber — Downloads** — apwp-ao card shows "Purchase Additional Slots" (no key field, no slots list).
3. **Lapsed subscriber — Dashboard** — Section visible, all content grayed and unclickable. "Reactivate Subscription" overlay button adds `ceil(occupied_slots / 5)` qty to cart (min 1).
4. **Lapsed subscriber — Downloads** — "Reactivate Subscription" button adds correct qty to cart.
5. **Never purchased — Dashboard** — No APWP-AO section rendered.
6. **Never purchased — Downloads** — "Purchase License" button shown.
7. **Slot release** — DELETE `/account/plugin-domains/{id}` fires correctly from dashboard; page reloads and slot disappears.
8. **Set Message** — TinyMCE modal opens, fetches current message, saves successfully.
9. **Qty edge cases** — 0 slots → qty 1 · 5 slots → qty 1 · 6 slots → qty 2 · 11 slots → qty 3.
