From f13fb348f93fa93ea09b29e9949a29981830f606 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 30 Jul 2026 15:19:04 +0200 Subject: [PATCH 1/2] fix(hosting/photo-storage): restore asset-canister recipe and fix canister-ID discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The example was broken twice over: 1. The switch to @dfinity/static-site (#1449) removed the legacy asset canister API (list/store/create_batch/commit_batch) that AssetManager from @icp-sdk/canisters/assets depends on — the app's entire purpose. Revert to @dfinity/asset-canister@v2.2.1, the only canister supporting programmatic uploads, with a comment explaining the exception (#1459). 2. The dfx-era canister-ID parsing broke on icp-cli's name-based frontend URLs (frontend.local.localhost) — Principal.fromText received "frontend.local" and threw at module load (white screen). Replace the hostname regex, ic0.app heuristic, and fetchRootKey() with the ic_env cookie pattern used by hello_world (safeGetCanisterEnv), which works on both URL forms and on mainnet. Also fix the README authorize command (canister is named frontend, not photo-storage) and point deploy instructions at the URL icp deploy prints. Verified end-to-end locally: gallery renders, uploads work after authorize, photos persist across reloads. Co-Authored-By: Claude Fable 5 --- hosting/photo-storage/README.md | 8 ++++---- hosting/photo-storage/icp.yaml | 6 +++++- hosting/photo-storage/src/App.jsx | 24 ++++++++++++++++-------- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/hosting/photo-storage/README.md b/hosting/photo-storage/README.md index 3e2e07061b..c2122f7484 100644 --- a/hosting/photo-storage/README.md +++ b/hosting/photo-storage/README.md @@ -38,19 +38,19 @@ Deploy the canisters: icp deploy ``` -The URL for the frontend depends on the canister ID. When deployed, the URL will look like this: +Open the frontend URL printed by `icp deploy`, e.g.: ``` -http://{canister_id}.localhost:8000 +http://frontend.local.localhost:8000 ``` To authorize an identity to upload files, it must be authorized first: ```bash -icp canister call photo-storage authorize '(principal "535yc-uxytb-gfk7h-tny7p-vjkoe-i4krp-3qmcl-uqfgr-cpgej-yqtjq-rqe")' +icp canister call frontend authorize '(principal "535yc-uxytb-gfk7h-tny7p-vjkoe-i4krp-3qmcl-uqfgr-cpgej-yqtjq-rqe")' ``` -> **Warning:** This example uses a hardcoded identity (defined in `src/App.js`). Before deploying to the IC mainnet, replace it with a proper authentication method such as [Internet Identity](https://docs.internetcomputer.org/building-apps/authentication/integrate-internet-identity). +> **Warning:** This example uses a hardcoded identity (defined in `src/App.js`). Before deploying to the IC mainnet, replace it with a proper authentication method such as [Internet Identity](https://docs.internetcomputer.org/guides/authentication/internet-identity). Stop the local network when done: diff --git a/hosting/photo-storage/icp.yaml b/hosting/photo-storage/icp.yaml index 9e53e7014e..4c709064c9 100644 --- a/hosting/photo-storage/icp.yaml +++ b/hosting/photo-storage/icp.yaml @@ -1,7 +1,11 @@ canisters: - name: frontend recipe: - type: "@dfinity/static-site@v0.3.1" + # Deliberately stays on the legacy asset-canister recipe: this example + # demonstrates programmatic uploads via AssetManager, which the + # static-site (certified-assets) canister does not support. + # See https://github.com/dfinity/examples/issues/1459 + type: "@dfinity/asset-canister@v2.2.1" configuration: dir: dist build: diff --git a/hosting/photo-storage/src/App.jsx b/hosting/photo-storage/src/App.jsx index bd1fcfa748..f8b2dd6dca 100644 --- a/hosting/photo-storage/src/App.jsx +++ b/hosting/photo-storage/src/App.jsx @@ -1,5 +1,6 @@ import {Ed25519KeyIdentity} from '@icp-sdk/core/identity'; import {HttpAgent} from '@icp-sdk/core/agent'; +import {safeGetCanisterEnv} from '@icp-sdk/core/agent/canister-env'; import {AssetManager} from '@icp-sdk/canisters/assets'; import {useEffect, useState} from "react"; import Masonry from "react-masonry-css"; @@ -8,16 +9,23 @@ import './App.css'; // Hardcoded principal: 535yc-uxytb-gfk7h-tny7p-vjkoe-i4krp-3qmcl-uqfgr-cpgej-yqtjq-rqe // Should be replaced with authentication method e.g. Internet Identity when deployed on IC const identity = Ed25519KeyIdentity.generate(new Uint8Array(Array.from({length: 32}).fill(0))); -const isLocal = !window.location.host.endsWith('ic0.app'); -const agent = HttpAgent.createSync({ - host: isLocal ? `http://127.0.0.1:${window.location.port}` : 'https://ic0.app', identity, -}); -if (isLocal) { - await agent.fetchRootKey(); + +// The ic_env cookie is set by the asset canister on all HTML responses. It +// contains the replica root key and the PUBLIC_* canister environment +// variables, so the app finds its own canister ID regardless of which URL the +// gateway serves it under (canister-id-based or name-based). +const canisterEnv = safeGetCanisterEnv(); +const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:frontend"] ?? new URLSearchParams(window.location.search).get('canisterId'); + +if (!canisterId) { + throw new Error("Canister ID for 'frontend' not found. Run 'icp deploy' first."); } -// Canister id can be fetched from URL since frontend in this example is hosted in the same canister as file upload -const canisterId = new URLSearchParams(window.location.search).get('canisterId') ?? /(.*?)(?:\.raw)?\.ic0.app/.exec(window.location.host)?.[1] ?? /(.*)\.localhost/.exec(window.location.host)?.[1]; +const agent = HttpAgent.createSync({ + host: window.location.origin, + rootKey: canisterEnv?.IC_ROOT_KEY, + identity, +}); // Create asset manager instance for above asset canister const assetManager = new AssetManager({canisterId, agent}); From 8135fba069aaa9c32371e670aa0f5e614af33842 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 30 Jul 2026 15:43:15 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(hosting/photo-storage):=20address=20rev?= =?UTF-8?q?iew=20=E2=80=94=20drop=20cookie-less=20fallback,=20fix=20README?= =?UTF-8?q?=20filename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ?canisterId= query-param fallback ran without the ic_env cookie and therefore without the local root key — a half-broken path nothing uses (the app is only ever served from the canister, which always sets the cookie; there is no dev-server flow). Require the cookie and fail with a clear error instead. Deliberately no fetchRootKey(): that is legacy behavior we no longer recommend — the root key comes from the certified ic_env cookie. Also fix the README warning to reference src/App.jsx (renamed from App.js in the Vite migration). Co-Authored-By: Claude Fable 5 --- hosting/photo-storage/README.md | 2 +- hosting/photo-storage/src/App.jsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hosting/photo-storage/README.md b/hosting/photo-storage/README.md index c2122f7484..e920eabfec 100644 --- a/hosting/photo-storage/README.md +++ b/hosting/photo-storage/README.md @@ -50,7 +50,7 @@ To authorize an identity to upload files, it must be authorized first: icp canister call frontend authorize '(principal "535yc-uxytb-gfk7h-tny7p-vjkoe-i4krp-3qmcl-uqfgr-cpgej-yqtjq-rqe")' ``` -> **Warning:** This example uses a hardcoded identity (defined in `src/App.js`). Before deploying to the IC mainnet, replace it with a proper authentication method such as [Internet Identity](https://docs.internetcomputer.org/guides/authentication/internet-identity). +> **Warning:** This example uses a hardcoded identity (defined in `src/App.jsx`). Before deploying to the IC mainnet, replace it with a proper authentication method such as [Internet Identity](https://docs.internetcomputer.org/guides/authentication/internet-identity). Stop the local network when done: diff --git a/hosting/photo-storage/src/App.jsx b/hosting/photo-storage/src/App.jsx index f8b2dd6dca..39825c3875 100644 --- a/hosting/photo-storage/src/App.jsx +++ b/hosting/photo-storage/src/App.jsx @@ -15,10 +15,10 @@ const identity = Ed25519KeyIdentity.generate(new Uint8Array(Array.from({length: // variables, so the app finds its own canister ID regardless of which URL the // gateway serves it under (canister-id-based or name-based). const canisterEnv = safeGetCanisterEnv(); -const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:frontend"] ?? new URLSearchParams(window.location.search).get('canisterId'); +const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:frontend"]; if (!canisterId) { - throw new Error("Canister ID for 'frontend' not found. Run 'icp deploy' first."); + throw new Error("Canister ID for 'frontend' not found. Run 'icp deploy' and open the URL it prints."); } const agent = HttpAgent.createSync({