APOLLO VISION LABS

Migrating to 0.2.0

What changes for an application on 0.1.x, in order, and what needs no attention at all.

0.2.0 adds the checklist and widens persistence to make room for it. If you use no storage prop, the upgrade is a version bump and nothing else. If you passed your own GuideStorage, one signature changes and one stored key moves.

What can be ignored

Nothing about tours changed. Tour, Step, GuideProvider, GuideTour, useTour, useGuideStep, the missing target policies, route matching, delegated navigation, translate and the labels on GuideTour are all as they were. The tour events are unchanged, and the three checklist events are additions to the GuideEvent union, not replacements.

The checklist is opt in. Not mounting ChecklistProvider leaves your application exactly where it was.

pnpm up @apollovisionlabs/guide-core @apollovisionlabs/guide-mui

Unpinned, because this page is about leaving 0.1.x and not about one target version. @apollovisionlabs/guide-mui has moved past 0.2.0 since: 0.3.0 adds a labels prop to Checklist and ChecklistLauncher so their own text can be translated. It adds a prop and breaks nothing, so it changes nothing in this migration; it is documented in Checklist.

Breaking change 1: GuideStorage is generic

0.1.x fixed the interface to one shape:

// 0.1.x
interface GuideStorage {
  read(tourId: string): Promise<TourProgress | null>
  write(tourId: string, progress: TourProgress): Promise<void>
}

0.2.0 makes both methods generic over the stored value, and the key becomes a plain string that the caller has already namespaced:

// 0.2.0
interface GuideStorage {
  read<T>(key: string): Promise<T | null>
  write<T>(key: string, value: T): Promise<void>
}

A custom implementation written against the old signature no longer typechecks. The fix is mechanical: make both methods generic and stop assuming the key is a tour id.

Before:

import type { GuideStorage, TourProgress } from '@apollovisionlabs/guide-core'

export function createApiStorage(baseUrl: string): GuideStorage {
  return {
    async read(tourId: string): Promise<TourProgress | null> {
      const response = await fetch(`${baseUrl}/tours/${tourId}/progress`)
      if (response.status === 404) return null
      return (await response.json()) as TourProgress
    },
    async write(tourId: string, progress: TourProgress): Promise<void> {
      await fetch(`${baseUrl}/tours/${tourId}/progress`, {
        method: 'PUT',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(progress),
      })
    },
  }
}

After:

import type { GuideStorage } from '@apollovisionlabs/guide-core'

export function createApiStorage(baseUrl: string): GuideStorage {
  return {
    async read<T>(key: string): Promise<T | null> {
      const response = await fetch(`${baseUrl}/guide/${encodeURIComponent(key)}`)
      if (response.status === 404) return null
      return (await response.json()) as T
    },
    async write<T>(key: string, value: T): Promise<void> {
      await fetch(`${baseUrl}/guide/${encodeURIComponent(key)}`, {
        method: 'PUT',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(value),
      })
    },
  }
}

Three things moved. The parameter is a key, not a tour id, so a route built as /tours/<id> no longer describes what arrives. The value is opaque, so a column named after stepIndex is now the wrong shape: store JSON keyed by (user, key). And the same instance will receive checklist:<id> keys the moment you mount a ChecklistProvider with it, which is the point of the change: one implementation, both features.

Breaking change 2: the storage key moved

0.1.x stored tour progress under the bare tour id. 0.2.0 stores it under tour:<id>.

Progress written by 0.1.x therefore sits at a key nothing reads any more. Nothing crashes: reading tour:product from a store that only holds product returns null, which is exactly the first visit case the provider already handles. A user who was in the middle of a tour when you shipped the upgrade restarts that tour once, from its first step. A user who had completed a tour sees it again if your application offers it to them.

You can leave the old entries where they are and accept that one restart, which is what the library assumes. If a restart is not acceptable in your application, migrate the rows on your side by copying each <id> to tour:<id> before the new version reaches users. There is no migration helper in the library, and the localStorage case is a short loop over your own namespace.

Behaviour change: stored values are validated

This one changes no signature, so nothing tells you about it at build time.

0.1.x passed whatever came back from storage into the tour state, checking only that status read in-progress. 0.2.0 checks the shape first, with isTourProgress, and rejects anything that fails. Checklist progress gets the same treatment through isChecklistProgress.

The consequence: an entry that is corrupted, truncated, hand edited, or written by a version of your own code that stored something else is now ignored, and the tour starts from its first step instead of resuming from a value it should never have trusted. If you saw occasional resumes onto an impossible step index in 0.1.x, that is the cause and it is now fixed.

Both guards are exported, so your own storage code can ask the same question the providers ask:

import { isTourProgress } from '@apollovisionlabs/guide-core'

const stored = await storage.read<unknown>('tour:product')
if (!isTourProgress(stored)) {
  // nothing usable at this key
}

In order

  1. Bump both packages to their current versions.
  2. If you pass a custom GuideStorage, widen read and write to the generic signature and treat the parameter as an opaque key. Typecheck: nothing else in the core changed, so a clean typecheck means you are done with the signature.
  3. Decide about the orphaned tour keys: accept one restart per user mid tour, or copy <id> to tour:<id> in your own store before release.
  4. Check nothing in your application depended on a corrupted entry resuming. In practice this means confirming that a tour starting from the beginning is acceptable where a stored value is unreadable.
  5. Optionally, add the checklist. It is a new provider and two new components, and it shares the storage instance you already have.