| name | migrate-adobe-app-to-template-v2 |
| description | Migrate an existing Adobe Express Add-on to template v2 conventions, including appRequest context injection, platform=adobe_addon, app_channel, Feishu/runtime error reporting, ErrorAlert server-translated errors, and Adobe sandbox fixes. Use when the user says ่ฟ็งป, ้้
, ๆฅๅ
ฅๆฐๆจกๆฟ, appRequest, ้ฃไนฆ, ErrorAlert, adobe-app ๅผๅ่ง่, sandbox, or template-v2 for an existing Adobe add-on. |
Migrate Adobe App to Template V2
Bring an existing Adobe Express Add-on onto the conventions defined by adobe-app-template-v2. The migration covers the same three core needs as canva-app-template-v2, plus a fourth that's Adobe-specific:
- Requests carry
uid / platform_uid / platform=adobe_addon / app_channel automatically
- Errors land in Feishu โ runtime / unhandledrejection / React render by default; API errors opt-in only
- Server messages translated by
Accept-Language are displayed via <ErrorAlert> instead of "ๆไฝๅคฑ่ดฅ่ฏท้่ฏ"
- Adobe sandbox compliance โ no
confirm/alert/prompt, no sp-picker for form state, no adobe_ prefix on userId, SWC versions unified, and no optional documentSandbox bridge unless the app already depends on it
Read references/code-patterns.md, references/file-manifest.md, and references/sandbox-checklist.md when the relevant phase below references them โ don't load them all up front.
How to invoke
The user usually says one of:
- "่ฟ็งป adobe-colorize-image ๅฐๆฐๆจกๆฟ"
- "็ป adobe-magic-eraser ๆฅไธ appRequest ๅ้ฃไนฆ"
- "ๆ adobe-img-translate ้้
ๅฐไธไธชๆฐ้ๆฑ"
- "่ฎฉ ่ท็ adobe-app-ๅผๅ่ง่ ่ตฐ"
Confirm the target app directory before doing anything. If the user gives an exact path, use it. If the user only gives an app name on this Mac, search likely roots such as /Users/hodohimehime/Desktop/code, /Users/hodohimehime/Desktop/vscode, /Users/hodohimehime/Downloads, and the current workspace, then confirm the matched directory before proceeding.
Related Adobe Skills
This skill remains the broad template-v2 migration workflow. When a migration exposes a specialized Adobe repair area, use the narrower skill without dropping this migration's template rules:
- Use
adobe-add-on-request-params for detailed public request params, request_context, wrappers, ownHosts, Accept-Language, and CSV ไผ ๅๆฐ updates.
- Use
adobe-add-on-login-loop-fix for repeated canvaid-login, canva-passport, /status, unstable callbacks, poll/login splitting, or fallback device IDs.
- Use
adobe-add-on-credits-flow for credits, free_cnt, paid credits, payment-return refresh, credit cache, balance drift, and successful-use deduction.
- Use
subscription-credits-guardrails when the migrated app has subscription monthly quota, subscribed footer totals, upgrade return, or monthly-then-purchased deduction.
- Use
adobe-addons-collaboration before editing YiChongmimi repos, and adobe-addons-push when the migrated project needs PR, packaging, or management sync.
Do not delete specialized optimization points from this migration skill. Instead, add or preserve cross-references so a migration can call the narrower skill for deeper rules.
Pipeline
The migration runs in five phases. Do them in order. Stop and check with the user at the end of each phase before continuing โ the in-between checkpoints save a lot of cleanup if something is off.
Phase 1: Detect โ produce migration report (what's there, what's missing, sandbox state)
Phase 2: Inject โ copy/refresh template infrastructure files
Phase 3: Wire entry โ ensure Provider chain + global error reporting + setRequestUserContext wiring
Phase 4: Migrate code โ rewrite API + error UI + sandbox cleanup
Phase 5: Verify โ npm run validate:config / lint:types / lint / package
Adobe Add-on entry differs from Canva โ there is no intent-shell vs legacy-shell distinction. Adobe always uses createRoot in src/index.tsx directly. So Phase 3 only needs to verify a single entry-file shape.
Phase 1: Detect
The goal here is to understand the current state without changing anything. Output a short migration report so the user can sanity-check scope.
Run these checks (use Bash, not Edit) and summarize.
Adobe Add-on shape (gates the rest):
| Check | Command | What it tells you |
|---|
Has src/manifest.json? | test -f <app>/src/manifest.json && echo yes | Required. Without it, this is not an Adobe Add-on โ stop and clarify with the user. |
Manifest sandbox excludes allow-modals | jq '.entryPoints[0].permissions.sandbox' <app>/src/manifest.json | If allow-modals is present, the project violates the sandbox-compliance rule. Flag for Phase 4c removal. |
Manifest declares documentSandbox? | jq -r '.entryPoints[0].documentSandbox // empty' <app>/src/manifest.json | Optional. Keep only if the pre-migration app already used it for required features. Do not add just because the template has src/code.js. |
| Entry file with createRoot | grep -l "createRoot" <app>/src/index.tsx 2>/dev/null | Adobe always has createRoot in src/index.tsx (no intent split). If createRoot isn't there, the app probably isn't a standard Adobe Add-on. |
| AddOnSdk usage | `grep -rn "AddOnSdk\ | adobe_sdk" /src --include='.ts' --include='.tsx' | head` |
Config-file naming (use whichever the app has; both are valid):
| Check | Command | What it tells you |
|---|
| Which config file does the app use? | ls <app>/src/config.ts <app>/src/app_config.ts 2>/dev/null | Most Adobe apps use src/app_config.ts (older scaffolding). The template uses src/config.ts. Whichever exists is the merge target; do not introduce a parallel file. Throughout this skill, "config.ts" refers to whichever the app actually has. |
Adobe SDK userId prefix bug (Pattern 13):
| Check | Command | What it tells you |
|---|
| getAdobeUserId still prefixes? | grep -n 'return "adobe_"|return \adobe_' /src/adobe_sdk.ts` | If matched, Phase 4 Pattern 13 must remove the prefix. Reason: backend expects canonical IDs (shared with Canva user system), prefix breaks calendar-spanning user identity. Reference: adobe/CANVA_TO_ADOBE_FORM_SYNC_NOTES.md ยง2. |
API layer location (don't assume src/api/ or src/apis/):
| Check | Command | What it tells you |
|---|
| Where do axios/fetch calls live? | grep -rlnE "axios\\.(post|get|put|delete)|\\bfetch\\(" <app>/src --include='*.ts' --include='*.tsx' | Phase 4 targets. Common spots: src/api/, src/apis/, src/utils/handle.ts, src/libs/upload_r2_cf.js, src/libs/fileToBase64.ts, even inline inside src/pages/. |
| Login endpoints | `grep -rln "canvaid-login\ | canva-passport" /src` |
Existing infrastructure parity:
| Check | Command | What it tells you |
|---|
| Has request_context? | test -f <app>/src/api/request_context.ts && echo yes | Required by Phase 2. |
| Has accept_language? | test -f <app>/src/api/accept_language.ts && diff -q <app>/src/api/accept_language.ts <template>/src/api/accept_language.ts | Must match the chosen template byte-for-byte. ~103-code dictionary is the backend translation contract. |
| Has feishu_report? | test -f <app>/src/utils/feishu_report.ts && echo yes | If yes, verify against template (token must not be embedded โ see next row). |
| Hardcoded Feishu token leaked? | grep -nE 'base64:[A-Za-z0-9+/=]{20,}' <app>/src/utils/feishu_report.ts 2>/dev/null | Must be removed in Phase 2 (overwrite from template). The base64:Ms... token should live in src/config.ts reporting.authToken only. |
| onUpdateUserStatus wired? | grep -rnE "onUpdateUserStatus=" <app>/src --include='*.tsx' | Must reference setRequestUserContext. If bound to inline (s) => { window.g_user_id = s.profile?.id } style code (common in older apps), Phase 3 must rewire to setRequestUserContext, otherwise public params and Feishu User ID break. |
| App channel registered? | (ask user) | Blocker โ productName must be in the same app_channel enum as Canva็ซฏ. Don't start Phase 2 without confirmation. |
| Manifest version | jq -r .version <app>/src/manifest.json | Phase 5 expects this to be bumped before packaging. |
Sandbox compliance state (Phase 4c targets):
| Check | Command | What it tells you |
|---|
| confirm/alert/prompt usage | grep -rnE '\\balert\\(|\\bconfirm\\(|\\bprompt\\(' <app>/src --include='*.ts' --include='*.tsx' | Every match must be replaced. Reference: CANVA_TO_ADOBE_FORM_SYNC_NOTES.md ยง3. |
| sp-picker form-state holders | grep -rnE 'sp-picker' <app>/src/pages <app>/src/components --include='*.tsx' | Every sp-picker holding form state (duration / aspect_ratio / model / mode / resolution) must convert to native <select>. Reference: notes ยง1. |
| AbortController coverage | grep -rnE 'AbortController|AbortSignal' <app>/src --include='*.ts' --include='*.tsx' | Every generate/upload entry point should thread signal to fetch / axios. If coverage is sparse, flag for Phase 4c. Reference: notes ยง5. |
| SWC version unification | grep '@spectrum-web-components' <app>/package.json | grep -oE '"[^"]+"$' | sort -u | Must produce exactly 1 version string. Mixed versions break SWC sub-package compatibility. Reference: notes ยง8. |
| MyAlert imperative usage | grep -rnE '<MyAlert\\b|alertRef\\.current\\?\\.show' <app>/src --include='*.tsx' | Phase 4b targets โ error catches that call alertRef.current?.show(...) must migrate to <ErrorAlert error={error} ... />. Non-error usage (upgrade prompt, policy) keeps MyAlert. |
| Catch sites without ErrorAlert | catches=$(grep -rnE "} catch \\(" <app>/src --include='*.tsx' --include='*.ts' | wc -l); rendered=$(grep -rlnE "<ErrorAlert\\b" <app>/src --include='*.tsx' | wc -l); echo "catches=$catches errorAlertFiles=$rendered" | After Phase 4, every API catch must reach a rendered <ErrorAlert>. |
Stale route / activeTab in error reports | grep -rnE "(route|activeTab):\\s*[\"']" <app>/src --include='*.tsx' --include='*.ts' | grep -v "currentRoute|updateCurrentRoute|router/" | These were removed from CanvaAppErrorReportParams (same struct Adobe uses). Drop per Pattern 12. |
genetate / succsss typos | grep -rnE "genetate|succsss|cacenl|recieve|seperat" <app>/src | Fix per Pattern 5. |
Report format:
## Migration plan for <adobe-app-name>
**Current state**
- manifest.json: present / sandbox excludes allow-modals: yes / no
- Entry file with createRoot: src/index.tsx
- Config file in use: src/config.ts OR src/app_config.ts
- getAdobeUserId() returns prefixed userId: yes / no (Pattern 13 target if yes)
- API call locations: <list of files / directories>
- Login endpoints (Tier 3, leave alone): <list>
- Has request_context: yes / no
- Has feishu_report: yes / no (token leaked at file:line if found)
- App channel: <productName> (enum status: confirmed / NOT REGISTERED)
- onUpdateUserStatus wired to setRequestUserContext: yes / no
- Manifest version: <x.y.z>
- Approx legacy axios/fetch sites: N
- Approx MyAlert / sp-toast / setAlert sites to migrate: M
**Sandbox compliance**
- confirm/alert/prompt calls: <count, files>
- sp-picker form-state holders: <count, files>
- AbortController gaps: <description>
- SWC versions: <unified / mixed: list versions>
**Phase 2 actions (Inject)**
- Copy: <list of files from references/file-manifest.md that are missing>
- Refresh: <list of files that exist but are stale>
- Skip: <list of files already at template parity>
**Phase 3 actions (Wire entry)**
- <specific file:line edits expected>
**Phase 4 migration scope**
- src/api/* or other API sites: <list of files with migration tier โ 1, 2, or 3>
- toast/Alert sites: <list of files>
- Pages with `catch` blocks that don't render `<ErrorAlert>`: <list of files>
- Pattern 13 prefix removal: yes / no
- Pattern 14 sandbox modal fixes: <count>
- Pattern 15 sp-picker fixes: <count>
**Risks / blockers**
- <e.g. app_channel not registered, custom webpack.config.cjs, mixed SWC versions>
Show this to the user. Wait for confirmation. Don't proceed if app_channel isn't confirmed registered.
Phase 2: Inject infrastructure
This phase is mechanical. Copy/refresh template files into the app. Read references/file-manifest.md for the exact list and per-file notes.
The general approach for each file:
- If the app does not have it โ copy from
adobe-app-template-v2 verbatim
- If the app has it but it differs โ diff against template, prefer template version, but preserve any app-local additions (e.g.
ownHosts may include a project-specific domain). Show the user the diff before overwriting.
- If the file has a hardcoded secret (Feishu token in
feishu_report.ts is the known case), always overwrite โ secrets in source belong in config.ts.
Critical rules:
config.ts / app_config.ts is special: don't overwrite it. The app's existing productName / appId / appName / baseUrl are sacred. Extend the existing config to add platform: "adobe_addon", request.ownHosts, reporting.* fields, and legacyReport.
webpack.config.cjs is special: don't overwrite. Many apps have project-specific tweaks (alias paths, asset rules). Just note "app has custom webpack config โ review manually."
manifest.json is special: don't overwrite. Audit sandbox to confirm it doesn't include allow-modals. Audit version for Phase 5.
adobe_sdk.ts: overwrite only if getAdobeUserId() still has the "adobe_" + userId prefix (Pattern 13 trigger).
- Don't touch business code in this phase โ
src/pages/, src/components/<app-specific>/, src/api/<feature>.ts. Those wait for Phase 4.
- After this phase, the app should still build and run; new infrastructure is present but not yet hooked up.
Run a quick smoke test at the end: cd <app> && npm run lint:types. Type errors here usually mean a copied file references a path that doesn't exist yet (often src/components/error_alert if you forgot to copy it). Resolve before moving on.
Phase 3: Wire the entry
Three things must end up in the entry layer:
installGlobalErrorReporting() is called inside AddOnSdk.ready.then(...) before createRoot(...).render(...) so first-paint JS errors don't slip through.
- Provider chain in
src/index.tsx (outer โ inner): IntlProvider โ AppContextProvider โ AdobeErrorBoundary โ <App />. Locale must be navigator.language || "en", not hardcoded "en" (otherwise Accept-Language header desyncs from UI language).
- In
src/app.tsx: <sp-theme> โ <AppLoadingLayout> โ <UserProvider onUpdateUserStatus={handleUpdateUserStatus}> โ <AppRouter>, where handleUpdateUserStatus is memoized with useCallback and calls both appContext.updateUserStatus(s) and setRequestUserContext(s).
The setRequestUserContext call in onUpdateUserStatus is the single most important wire โ without it, window.g_user_id / g_platform_uid won't get populated, all public params will be empty, and Feishu cards will say User ID: not logged in even for logged-in users.
Never pass an inline callback to UserProvider.onUpdateUserStatus. Existing Adobe UserProvider implementations often have login = useCallback(..., [onUpdateUserStatus, productName]) and a useEffect(() => refreshUserStatus(true), [refreshUserStatus]). An inline callback changes identity on every render, which changes login, then refreshUserStatus, then reruns the login effect and can spam /api/user/canvaid-login. Always use useCallback; if you copy/refresh AppContextProvider, also stabilize its methods with useCallback and its context value with useMemo.
After this phase, open the app in real Adobe Express (not a standalone browser) once and verify:
- Login flow still works
- DevTools Network shows new requests to ownHosts have
uid / platform_uid / platform=adobe_addon / app_channel on the URL query
Accept-Language header is a canonical code (e.g. en, zh-CN, es-419), not raw en-US,en;q=0.9
- Throwing a manual
Promise.reject(new Error("test")) triggers a Feishu card; remove any temporary test UI/hook immediately after verification
- Feishu card body shows a real
User ID (not not logged in). If it shows not logged in after the user is authenticated, UserProvider.onUpdateUserStatus is not calling setRequestUserContext โ go back and fix it before Phase 4.
- Network does not repeatedly call
/api/user/canvaid-login after the initial login/status bootstrap. If it repeats, inspect callback identity first (onUpdateUserStatus, AppContext methods/value), not the backend.
If any of those fail, fix before Phase 4.
Phase 4: Migrate business code
This is the only phase where you touch business logic. Go file by file, show diffs to the user, get confirmation, then apply. Do not bulk-rewrite โ every existing API call has small variations that matter.
4.0 Request-contract guardrail (mandatory pre-edit ritual)
Before changing any API function, do these three things in order. No edits until all three are done โ if you skip the table you will silently drop auth headers and body identity fields.
- Read the OLD file in full. Do not rely on memory or grep snippets.
- Write the contract table below for every exported function โ into the conversation, not into a code comment.
- Diff old vs new in your head: if a header / body field / token source disappears, the table must show why (with verification). If you cannot justify the removal, keep it.
appPost / appFetch / appAxios only add public query params and Accept-Language; they do not replace endpoint-specific auth, headers, or body fields. The new public params are additive, not a substitute for legacy fields.
For every migrated request, fill in this table:
| Field | Must compare |
|---|
| URL + host | api.imgkits.com vs ai.imgkits.com vs usa.imgkits.com โ preserve per call |
| Method | GET/POST/PUT and upload-specific raw fetch behavior |
| Headers | Authorization, Channel, X-Access-Ticket, Content-Type, custom tokens |
| Query | existing query params plus new public params; don't overwrite existing params |
| Body | canva_id (still the contract key for Adobe too!), user_id, appid, passport, business fields |
| Response shape | data, data.data, polling status, etc. |
Adobe-specific hard rules (in addition to all Canva rules):
- Never delete
X-Access-Ticket: <passport> header on /api/user/status, /api/user/reduce-free-cnt, /api/user/add-monthly-usage. Passport is the only auth those three endpoints accept.
- Never delete
canva_id body field on canvaid-login and canva-passport. The backend reads it by that exact key โ Adobe็ซฏ sends the Adobe userId under canva_id, that's by design.
- Never auto-inject public params on login bootstrap endpoints (
canvaid-login / canva-passport). These bootstrap the very fields the interceptor would add. They must stay on raw fetch (Tier 3) โ that's what src/user/request.ts is for in the template.
4a. API call sites: three migration tiers
Adobe Add-on uses axios interceptor instead of forcing every call to switch to appPost. Each call site gets one of three tiers:
| Tier | Old code | New code | When |
|---|
| Tier 1 โ recommended | axios.post(url, body) then check data.code !== 0 | appPost<T>(url, body) (drops the check; appPost throws ApiError) | Default. New code, or files where business-failure-as-throw is desired. |
| Tier 2 โ minimal change | import axios from "axios" + axios.post(url, body) | import { appAxios as axios } from "src/api/axios_instance" (no other change) | Files that directly inspect response.data.code and need to preserve the original throw semantics. Interceptor still injects public params + Accept-Language. |
| Tier 3 โ skip | fetch(presignedUrl) / login bootstrap | Leave as raw fetch. Optionally use appFetch(..., { skipRequestContext: true }) for the type benefit. | R2/S3 presigned PUT uploads, canvaid-login, canva-passport. Interceptor would break these. |
Default to Tier 1. Drop to Tier 2 if:
- The function inspects
response.data.code for branching (not just throw).
- The function returns the raw response shape to callers.
- The function is heavily tested with the existing throw behavior.
Drop to Tier 3 if:
- The URL is a presigned upload (R2 / S3 / OSS).
- The file is
src/user/apis.ts / src/login/apis/ (passport bootstrap).
After migration, see references/code-patterns.md Patterns 1, 1b, 2 for full before/after examples.
4a-bis. UI preservation rule (do not redesign while migrating)
Migration is API + error display + reporting + sandbox compliance. Nothing else. In any file you touch in Phase 4:
- โ Don't replace
<img> with <MyImageCard> or any custom component. If the old code rendered raw <img>, keep it.
- โ Don't restructure
<sp-theme> / div layout because it "looks cleaner".
- โ Don't swap
sp-button variants, don't change disabled patterns, don't change style props.
- โ Don't merge two state hooks into one, don't extract subcomponents, don't rename props.
- โ
The only allowed UI edits in Phase 4 are:
- Pattern 3 / 4b: replacing error display (MyAlert.show โ ErrorAlert)
- Pattern 14: replacing
confirm/alert/prompt (use inline dialog or remove confirmation)
- Pattern 15: replacing
sp-picker with native <select> for form state
- 4c: removing dev-only debug buttons
If you find yourself "improving" a component while migrating it, stop and revert.
4b. Error UI sites: MyAlert.show / sp-toast / setAlert โ <ErrorAlert>
The rule: every API failure the user could observe must reach a <ErrorAlert> showing the backend's translated msg. No silent catches, no console.error-only paths.
For every catch that follows an appPost / appFetch / appAxios / appRequest call:
- Replace the existing error display with
setError(error) in catch.
- Render
<ErrorAlert error={error} fallback={intl.formatMessage({ defaultMessage: ..., description: ... })} onDismiss={() => setError(null)} /> somewhere in the page that's visible when the failure happens.
- The fallback should keep the original copy (e.g. "Translation failed. Please try again.") wrapped in
intl.formatMessage for localization.
- Add
if ((e as Error)?.name === "AbortError") return; before setError(e) to avoid alerting on cancel.
- Remove any inline
reportAdobeAppError({source: "api"}) calls in catch blocks. API errors aren't auto-reported; use appPost(..., { reportApiErrors: true, reportContext: {...} }) opt-in for genuinely critical paths.
<MyAlert> is not deleted โ it still handles non-error info messages (subscription upsell, policy links, credit warnings). Only error paths migrate to <ErrorAlert>.
The four explicitly-allowed exceptions โ if a catch block fits one of these, it doesn't need <ErrorAlert>:
| Exception | What's OK | What's still required |
|---|
| Polling catch that retries silently | Mid-poll transient errors | Terminal failure (after maxCount) MUST surface to user |
| Cleanup-only handler | try { cancelTask(); } catch {} on unmount | Original failure already shown elsewhere |
| Optional enrichment | Non-critical side request (analytics ping) | Comment why; console.warn-only, not silent |
| Outer catch that re-throws | catch (e) { log(e); throw e; } | Outermost user-action handler must render <ErrorAlert> |
Document each exception in the migration summary.
4c. Adobe sandbox cleanup (new vs Canva)
After file-by-file edits, run these checks. All must come back clean:
grep -rnE '\balert\(|\bconfirm\(|\bprompt\(' src --include='*.ts' --include='*.tsx' โ empty (Pattern 14)
grep -rn "sp-picker" src/pages src/components --include='*.tsx' โ form-state-holding ones converted to native <select> (Pattern 15)
grep -rn 'return "adobe_"' src/adobe_sdk.ts โ empty (Pattern 13)
- AbortController coverage on every generate/upload entry point (notes ยง5)
grep '@spectrum-web-components' package.json | grep -oE '"[^"]+"$' | sort -u โ exactly 1 line (notes ยง8)
grep -rnE "(route|activeTab):\\s*[\"']" src --include='*.tsx' --include='*.ts' | grep -v "currentRoute|updateCurrentRoute|router/" โ empty (Pattern 12)
grep -rnE "genetate|succsss|cacenl|recieve|seperat" src โ empty (Pattern 5)
grep -rn "console.log" src โ reviewed (Terser drops them in prod, but lint may flag)
See references/sandbox-checklist.md for sandbox specifics indexed against CANVA_TO_ADOBE_FORM_SYNC_NOTES.md.
4c-ter. Optional document sandbox bridge guardrail
documentSandbox is not part of the default migration. Do not add or keep template-style panelโdocument-sandbox bridges (src/code.js, runtime.exposeApi, runtime.apiProxy, editor.context.on, selection auto-import) unless the app had a required pre-existing feature that depends on document selection or page-node access.
Why: in Adobe Express, sandbox callback dispatch can surface as Feishu cards like Adobe unhandled promise rejection with TypeError: cannot read property 'apply' of undefined from add-on-sdk-document-sandbox -> dispatchEvent -> callback. If the migrated app doesn't need canvas-selection auto-import, remove entryPoints[0].documentSandbox from src/manifest.json and make any setupCanvasSelectionAutoUpload-style helper a no-op or remove its caller.
If the app truly needs document sandbox features:
- Keep the bridge pull-based and user/action initiated; avoid automatic
editor.context.on(selectionChange, ...) registration during startup.
- Wrap every
editor access and apiProxy call in try/catch; never let sandbox errors become unhandled rejections.
- Add a real-Adobe cold-start test: no Feishu card on panel open, no repeated login requests, and no
apply of undefined card after closing/reopening the panel.
4c-bis. Feishu token leak check (scoped)
The canonical base64:MsUJo+... token IS supposed to live in src/config.ts / src/app_config.ts reporting.authToken. The check:
grep -nE 'base64:[A-Za-z0-9+/=]{20,}' src/utils/feishu_report.ts 2>/dev/null \
&& echo "โ token leaked into feishu_report.ts โ overwrite from template"
grep -rnE 'base64:[A-Za-z0-9+/=]{20,}' src --include='*.ts' --include='*.tsx' \
| grep -vE "(src/config\.ts|src/app_config\.ts):" \
&& echo "โ token found outside config โ investigate" \
|| echo "โ token only lives in config"
Phase 5: Verify
Run these in order. Each must pass before declaring the migration done.
cd <adobe-app>
npm install
npm run validate:config
npm run lint:types
npm run lint
npm test
npm run package
unzip -p dist.zip manifest.json | jq .version
grep -rnE 'Feishu report test|Test Feishu Error|Promise\.reject\(new Error\("test' src --include='*.ts' --include='*.tsx' \
&& echo "โ remove manual Feishu test code" \
|| echo "โ no manual Feishu test code"
Then manual sanity pass in real Adobe Express (not standalone browser โ sandbox restrictions only apply in Express):
1. Public params (positive check) โ DevTools Network on any business request to *.imgkits.com / livepolls.app. Confirm:
- URL has
uid / platform_uid / platform=adobe_addon / app_channel=<productName>
- Headers has
Accept-Language: <canonical-code> (one of en / zh-CN / zh-TW / es-419 / pt-BR / pt-PT / etc., not raw en-US,en;q=0.9)
If you see un-normalized values, the request is bypassing request_context.ts (likely a stray axios.post / fetch that didn't get migrated).
2. Request parity (negative check, per migrated function) โ for every endpoint you migrated in Phase 4, run a real request and check against the contract table from 4.0. Use this checklist:
Endpoint: <function name @ file>
โ URL host matches old (api.imgkits.com vs ai.imgkits.com)
โ Method matches old
โ Headers โ every header in the old contract is present:
โ Authorization: Bearer ... (when old code sent one)
โ Channel: ... (when old code sent one)
โ X-Access-Ticket / passport headers (login + credit endpoints)
โ Content-Type unchanged
โ Body โ every body field in the old contract is present:
โ canva_id (yes, even for Adobe โ it's the contract key)
โ user_id / appid / canva_user_token / passport (identity)
โ business fields unchanged in shape and naming
โ New public query params present (uid / platform_uid / platform=adobe_addon / app_channel)
โ Response success path: the success-shape access still resolves
โ Response failure path: backend error message surfaces in ErrorAlert
โ Polling endpoints: success/fail enum unchanged; poll host unchanged
3. Adobe sandbox compliance โ in real Adobe Express:
- Trigger any UI that used to call
confirm() โ no silent failure, action runs as expected
- Trigger any flow that used
sp-picker form โ request body contains the selected value (not the default)
- Network tab: Adobe
userId in canvaid-login payload does not have adobe_ prefix
localStorage.getItem("<userId>_passport") exists after login
4. iframe cache pitfall โ close the Add-on panel and reopen between rounds. Notes ยง9: iframe sometimes caches old code; "reload panel" inside the iframe isn't enough.
5. Multilingual <ErrorAlert> โ for each migrated endpoint, force a real backend failure in at least two languages. Switch via:
- Adobe Express UI language setting (most reliable โ propagates to
navigator.language in the panel iframe)
- DevTools console:
Object.defineProperty(navigator, 'language', { get: () => 'ja' }) then reload
- NOT
document.documentElement.lang = "..." โ accept_language.ts reads from navigator.language, not the DOM
For each language, <ErrorAlert> must show the backend's translated msg verbatim โ not the fallback string. If you see fallback for a known-failing input, the page bypasses ErrorAlert.
6. Cancel doesn't spam โ start a long request, hit cancel. No Feishu card should appear.
7. Real runtime error reaches Feishu โ temporarily insert throw new Error("migration smoke") in some component lifecycle. Feishu ROBOT_MONITOR receives it. Confirm User ID field shows actual id (not not logged in).
8. Manifest version bump โ unzip -p dist.zip manifest.json | jq .version matches src/manifest.json version. If you forgot to bump before packaging, the deploy will look successful but Express clients will keep loading the old version (notes ยง8).
Then audit grep โ these must all be clean:
cd <adobe-app>
grep -rnB2 -A6 -E "await app(Post|Fetch|Get|Request)" src --include='*.tsx' --include='*.ts' \
| grep -B6 "catch" \
| grep -vE "setError|throw|AbortError" \
|| echo "โ no bypassing catches"
for f in $(grep -rlnE "appPost|appFetch|appAxios" src/pages src/components --include='*.tsx' 2>/dev/null); do
grep -q "ErrorAlert" "$f" || echo "โ $f uses appAxios family but does not import ErrorAlert"
done
for f in $(grep -rln "import.*ErrorAlert" src --include='*.tsx' 2>/dev/null); do
grep -qE "<ErrorAlert(\\s|/|>)" "$f" || echo "โ $f imports ErrorAlert but never renders <ErrorAlert ...>"
done
grep -rnB1 -A3 'alertRef\.current\?\.show' src --include='*.tsx' \
| grep -B3 -E 'tone:\s*"negative"' \
&& echo "โ MyAlert.show with negative tone โ convert to ErrorAlert"
grep -rnE '\balert\(|\bconfirm\(|\bprompt\(' src --include='*.ts' --include='*.tsx' \
&& echo "โ sandbox-blocked native modal call found โ Pattern 14" \
|| echo "โ no native modal calls"
grep -rn "sp-picker" src/pages src/components --include='*.tsx' \
&& echo "โฉ sp-picker present โ verify it doesn't hold form state (Pattern 15)" \
|| echo "โ no sp-picker"
grep -n 'return "adobe_"\|return `adobe_' src/adobe_sdk.ts \
&& echo "โ adobe_ prefix still in getAdobeUserId โ Pattern 13" \
|| echo "โ no adobe_ prefix"
grep -rnE "onUpdateUserStatus=" src --include='*.tsx' \
| grep -v "setRequestUserContext" \
&& echo "โ onUpdateUserStatus is NOT wired to setRequestUserContext" \
|| echo "โ onUpdateUserStatus correctly wired"
grep -rnE "(route|activeTab):\\s*[\"']" src --include='*.tsx' --include='*.ts' \
| grep -vE "currentRoute|updateCurrentRoute|router/" \
&& echo "โ stale route/activeTab in reporter calls โ Pattern 12" \
|| echo "โ no stale route/activeTab"
When everything passes, summarize for the user:
- Files changed (with per-tier breakdown for API migrations)
- Files removed (e.g. embedded Feishu token, debug logs)
- Sandbox cleanup count (modals replaced / sp-picker converted / prefix removed)
- Verification checklist results
- Anything intentionally skipped (e.g. an endpoint with custom auth that wasn't migrated)
Things this skill does NOT do
To keep blast radius bounded, the skill stays out of:
- Footer / PayLink / pages restructuring โ if the app has them, leave them.
- Adding routes / business pages โ no UI redesign.
- Schema changes to backend responses โ if
userMessage / msg is missing, file a backend ticket.
- Replacing UserProvider implementation โ too disruptive; only re-wire
onUpdateUserStatus to call setRequestUserContext.
- React 18 โ 19 upgrade โ out of scope.
- Webpack config consolidation โ too easy to break per-project tweaks.
- i18n hardening โ if
IntlProvider locale="en" is hardcoded, flag in Phase 1 report but don't auto-fix.
If the user asks for those, point them at a separate task. Finish what's in scope here first.
Related docs
<adobe-workspace>/CANVA_TO_ADOBE_FORM_SYNC_NOTES.md โ Patterns 13/14/15 + Phase 4c sandbox checklist authoritative source when present
<adobe-workspace>/adobe-app-template-v2/ or another user-confirmed template path โ source of truth for files to copy
- A sister Canva template-v2 migration skill may be useful for cross-reference when present