Skip to content

[Shopify] Migrate Shopify Connector to Expiring Offline Access Tokens#9327

Draft
onbuyuka wants to merge 5 commits into
mainfrom
bugs/637954-migrate-shopify-expiring-tokens
Draft

[Shopify] Migrate Shopify Connector to Expiring Offline Access Tokens#9327
onbuyuka wants to merge 5 commits into
mainfrom
bugs/637954-migrate-shopify-expiring-tokens

Conversation

@onbuyuka

@onbuyuka onbuyuka commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Background

As of December 2025, Shopify supports expiring offline access tokens with refresh-token rotation. Public apps created before 2026-04-01 must migrate to expiring tokens by 2027-01-01; after that date, REST/GraphQL Admin API requests made with non-expiring tokens are rejected. The Shopify Connector currently uses non-expiring offline tokens and must be migrated.

Token characteristics (Shopify):

  • Access token lifetime: 1 hour (expires_in: 3600)
  • Refresh token lifetime: 90 days (refresh_token_expires_in: 7776000)
  • Migration is a one-time, irreversible token exchange per shop (the original non-expiring token is revoked on success)
  • Only one refreshable expiring token per app + store

Refs: About offline access tokens - Migrating from non-expiring to expiring tokens

Approach

Token validity is centralized in a single orchestrator, EnsureValidAccessToken(Store), invoked from Shpfy Communication Mgt.GetAccessToken - so every API call (interactive and background) transparently migrates a legacy token or refreshes an expiring one before use. A scheduled backstop job additionally keeps idle shops access/refresh tokens alive and completes migrations for shops that never make an on-demand call.

Changes

Token storage - Shpfy Registered Store New (table 30138)

  • New fields Token Expires At / Refresh Token Expires At; refresh token kept in IsolatedStorage.

Acquisition, refresh & migration - Shpfy Authentication Mgt. (codeunit 30199)

  • New installs request expiring tokens (expiring=1).
  • EnsureValidAccessToken orchestrates migrate-if-legacy / refresh-if-near-expiry, serialized per shop.
  • RefreshAccessToken (grant_type=refresh_token) with transient retry and a terminal reconnect error.
  • MigrateToExpiringToken (one-time token exchange, best-effort - keeps the working token on failure).
  • ForceTokenRefresh, IsRefreshTokenExpired, and a Shop Card reconnect notification.

Consumption - Shpfy Communication Mgt.

  • GetAccessToken invokes the orchestrator; reactive 401 -> refresh -> retry in ExecuteWebRequest.

Scheduled backstop - Shpfy Token Refresh (30431) + Shpfy Token Refresh Shop (30432)

  • Dispatcher (TableNo = Job Queue Entry) iterates enabled shops and runs the per-shop worker (TableNo = Shpfy Shop) via Codeunit.Run for isolation; recurring Job Queue entry registered via installer + upgrade.

UX - Shpfy Shop Card

  • Reconnect notification when the refresh token has expired.

Docs & tests

  • Shpfy Token Refresh Test unit tests; living docs refreshed (business-logic, data-model, patterns, CLAUDE.md).

Telemetry

Emitted for migration success/failure, refresh success/transient/terminal and backstop per-shop failures, under event IDs 0000UIV-0000UJ1.

Test plan

  • Shopify App project compiles clean.
  • Shpfy Token Refresh Test unit tests for the expiry decisions.
  • Install / refresh / migration / backstop / notification scenarios validated locally.

Fixes AB#637954

Adds support for Shopify expiring offline access tokens (public apps must
migrate by 2027-01-01):

- Persist token/refresh expiry on "Shpfy Registered Store New" and store the
  refresh token in IsolatedStorage.
- Request expiring tokens on install (expiring=1), refresh before expiry, and
  migrate legacy non-expiring tokens via token exchange (best-effort).
- Orchestrate on-demand from "Shpfy Communication Mgt." (GetAccessToken) with a
  reactive 401 refresh-and-retry.
- Scheduled backstop job ("Shpfy Token Refresh" + per-shop worker) to keep
  tokens and 90-day refresh tokens alive; registered via installer/upgrade.
- Shop Card reconnect notification when the refresh token has expired.
- Tests for the refresh-token expiry decision logic.

NOTE: "Shpfy Token Dev Tools" (page 30440) is temporary test scaffolding and
must be removed before merge.

Fixes AB#637954

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fe0c18c-7f03-4f1f-880f-9adb9eb1dfcc
@github-actions github-actions Bot added the AL: Apps (W1) Add-on apps for W1 label Jul 11, 2026
@github-actions github-actions Bot added this to the Version 29.0 milestone Jul 11, 2026
onbuyuka and others added 4 commits July 11, 2026 17:06
- Remove the temporary "Shpfy Token Dev Tools" page (30440) and its permission-set entry.
- Clear the new telemetry event IDs (set to '') so they can be assigned by the tagging script.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fe0c18c-7f03-4f1f-880f-9adb9eb1dfcc
Refresh the living docs (al-docs update) to cover slice 637954:
- business-logic.md: new "Authentication and token lifecycle" section.
- data-model.md: Shpfy Registered Store New token/expiry storage + ER entry.
- patterns.md: Job Queue dispatcher/worker per-shop isolation pattern.
- CLAUDE.md (app + Base): auth overview and "things to know".

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fe0c18c-7f03-4f1f-880f-9adb9eb1dfcc
Remove the LogTokenTelemetry helper and inline Session.LogMessage('', ...) at
each call site so the telemetry tagging script can assign event IDs (it only
fills literal Session.LogMessage first-args). Matches the existing connector
convention. Tags left empty pending the script.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fe0c18c-7f03-4f1f-880f-9adb9eb1dfcc
Fill the token lifecycle telemetry event IDs (0000UIV-0000UJ1) via the tagging script.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5fe0c18c-7f03-4f1f-880f-9adb9eb1dfcc
end;

local procedure SaveStoreInfo(Store: Text; ActualScope: Text; AccessToken: SecretText)
local procedure SaveInstalledToken(Store: Text; ResponseBody: Text)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non debuggable?

JobQueueEntry."No. of Minutes between Runs" := 720;
JobQueueEntry."No. of Attempts to Run" := 5;
JobQueueEntry.Description := CopyStr(JobDescriptionTxt, 1, MaxStrLen(JobQueueEntry.Description));
JobQueueEntry."Job Queue Category Code" := JobQueueCategoryLbl;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk that it will be in the same category as pricing or inventory, which might take 1 hour to complete

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AL: Apps (W1) Add-on apps for W1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants