|
|
||
|---|---|---|
| app | ||
| docs | ||
| extensions/thank-you-activity | ||
| prisma | ||
| scripts | ||
| test | ||
| .env.example | ||
| .gitignore | ||
| README.md | ||
| package-lock.json | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| shopify.app.toml | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
README.md
Dreame Shopify App
Shopify custom app scaffold for the Dreame Thank You Page activity flow.
This project follows:
/初始化需求/追觅Shopify App Codex编码方案指令.md- Scope: local runnable code and deployment documentation only.
- Out of scope: real deployment, production database provisioning, real third-party API integration, real test orders.
What This App Does
- Customer lands on Shopify Thank You Page.
- Checkout UI Extension target
purchase.thank-you.block.renderloads. - Extension gets
orderId,orderNumber,locale, and a Shopify session token. - Extension calls
POST /api/action/activity/check. - Backend validates the checkout session token, queries order data through Admin GraphQL, reads the
$app:dreame_activityMetaobject config, checks order-level idempotency, calls Dreame status API, then calls Dreame push API. - On success, backend returns
success,redirectUrl, and localizedbuttonText. - Extension renders a button and opens
redirectUrlin the current window.
Failures return { "success": false }; the extension renders nothing and does not block the native Thank You Page.
Local Setup
The Shopify CLI was not available in this workspace, and remote npm create @shopify/app@latest execution was blocked by local safety policy. The project has therefore been initialized as a local scaffold matching the requested Shopify Remix structure.
When network access is available:
cd "/Users/jason/Downloads/myproject/Shopify 插件开发/dreame-shopify-app"
npm install
cp .env.example .env
npx prisma generate
npx prisma migrate dev
shopify app dev
Install the app to the development shop:
function-613wh0ez.myshopify.com
Environment
Copy .env.example to .env and fill Shopify credentials:
SHOPIFY_API_KEY=
SHOPIFY_API_SECRET=
SHOPIFY_APP_URL=
SCOPES=read_orders,read_metaobjects,write_metaobjects,write_metaobject_definitions
DATABASE_URL="file:./dev.sqlite"
THIRD_PARTY_MODE=mock
THIRD_PARTY_TIMEOUT_MS=5000
LOG_RETENTION_DAYS=7
METAOBJECT_TYPE=$app:dreame_activity
METAOBJECT_HANDLE=dreame-activity-config
THIRD_PARTY_MODE=mock is the default because third-party APIs, signing samples, and test orders were not provided.
Metaobject Configuration
The app-owned Metaobject definition is versioned in shopify.app.toml:
- TOML key:
[metaobjects.app.dreame_activity] - Admin API type:
$app:dreame_activity - Suggested handle:
dreame-activity-config - Admin access:
merchant_read_write - Storefront access:
none
Use METAOBJECT_TYPE=dreame_activity only if the shop already uses a legacy merchant-owned Metaobject definition.
Fields:
| Key | Type | Required | Notes |
|---|---|---|---|
activity_id |
number_integer |
yes | Dreame activity ID |
api_app_id |
single_line_text_field |
yes | Sent as dreame-api-app-id |
api_secret |
single_line_text_field |
yes | Backend only; never expose to extension |
status_api_url |
url |
yes | Activity status API |
push_api_url |
url |
yes | Activity push API |
status_success_code |
number_integer |
no | Status API success code; default 0 |
status_active_required |
boolean |
no | Whether Status API must return data.status=true; default true |
push_success_code |
number_integer |
no | Push API success code; default 0 |
redirect_url |
url |
yes | Button target URL |
button_text |
json |
yes | Locale map |
shop |
single_line_text_field |
no | Overrides properties.shop; fallback is myshopify domain |
Example button_text:
{
"en": "Claim Your Reward",
"zh-CN": "领取奖励",
"zh-TW": "領取獎勵",
"de": "Belohnung erhalten",
"fr": "Réclamer votre récompense"
}
Optional setup script:
SHOPIFY_SHOP_DOMAIN=function-613wh0ez.myshopify.com \
SHOPIFY_ADMIN_ACCESS_TOKEN=shpat_xxx \
npx tsx scripts/setup-metaobject.ts
The optional script upserts the singleton Metaobject entry. The definition itself is managed through shopify.app.toml and applied by Shopify CLI during shopify app dev or shopify app deploy.
API Contract
Endpoint:
POST /api/action/activity/check
Headers:
Authorization: Bearer <checkout session token>
Content-Type: application/json
Request:
{
"orderId": "gid://shopify/Order/1234567890",
"orderNumber": "#1001",
"locale": "zh-CN"
}
Success response:
{
"success": true,
"redirectUrl": "https://activity.example.com",
"buttonText": "领取奖励"
}
Failure response:
{
"success": false
}
CORS headers are returned for POST and OPTIONS; source trust relies on Shopify checkout session token validation.
Dreame Signing
All third-party requests include:
dreame-api-app-iddreame-api-timestampdreame-api-sign
Signature:
sign = MD5(bodyString + timestamp + api_secret)
The implementation serializes the payload once with JSON.stringify(payload). The exact same bodyString is used both for signing and as the fetch body.
Mock Mode
Default mock behavior:
- Status API returns
code=0anddata.status=true. - Push API returns
code=0. - Live success criteria can be overridden through Metaobject fields
status_success_code,status_active_required, andpush_success_code.
Useful mock environment variables:
DREAME_MOCK_STATUS_CODE=0
DREAME_MOCK_STATUS_ACTIVE=true
DREAME_MOCK_PUSH_CODE=0
DREAME_MOCK_TIMEOUT=false
Switch to live calls only after third-party endpoints and credentials are available:
THIRD_PARTY_MODE=live
Database
Development uses SQLite:
DATABASE_URL="file:./dev.sqlite"
Production should use Postgres. The same Prisma schema applies after changing the datasource provider and connection string as part of production hardening.
Tables added by this scaffold:
ActivityPushLog: one row per status/push attempt or failure.ActivityPushIdempotency: unique key onshop + orderId + activityId; prevents duplicate push for the same order and activity.
Logs are sanitized before persistence and must not store api_secret or dreame-api-sign.
Checkout UI Extension
Location:
extensions/thank-you-activity
Target:
purchase.thank-you.block.render
Capabilities:
[extensions.capabilities]
network_access = true
Set the extension setting app_url to the public HTTPS backend URL. During shopify app dev, this is the tunnel URL produced by Shopify CLI.
purchase.thank-you.block.render is a valid latest Thank You Page block target. Merchants can control placement in the checkout and accounts editor; changing to a static target such as purchase.thank-you.header.render-after requires TOML/code review and redeployment.
The extension sets default_placement = "ORDER_STATUS1" so the first suggested placement is above the order status card. Merchants can still move the block in the checkout and accounts editor.
Tests
After dependencies are installed:
npm test
npm run typecheck
Current test coverage:
test/signature.test.ts: MD5 signature and single-serialization body contract.test/idempotency.test.ts: same order/activity does not push twice and still returns button data.test/config.test.ts: app-owned Metaobject default type and legacy type override.test/activity-flow.test.ts: inactive status, non-zero status code, configurable success codes, configurable status active policy, successful push payload shape, push failure, missing email, missing customer ID.
Deployment Steps
This task does not deploy. For production deployment later:
- Provision Postgres.
- Set production environment variables.
- Update
shopify.app.tomlURLs. - Run Prisma migration against production.
- Run
shopify app deploy. - Publish the Checkout UI Extension.
- Configure the extension
app_urlsetting. - Deploy the app-owned
$app:dreame_activitydefinition and upsert thedreame-activity-configentry. - Switch
THIRD_PARTY_MODE=live. - Run end-to-end validation with real test orders and third-party test endpoints.
Security Notes
api_secretis backend-only.- The extension never reads Metaobject fields directly.
- Third-party calls are always made by the backend.
- Frontend-provided order data is not trusted; backend queries Shopify Admin GraphQL.
- Logs are sanitized and pruned after
LOG_RETENTION_DAYSdays.
Acceptance Coverage
| Requirement | Coverage |
|---|---|
| Thank You Page loads extension | extensions/thank-you-activity |
| Page automatically calls backend | Checkout.tsx effect |
| Backend reads configuration | config.server.ts |
| Status API called | dreame-client.server.ts, activity.server.ts |
status=true calls push API |
activity.server.ts, tests |
Push success returns success=true |
activity-flow.test.ts |
| Button displays | Checkout.tsx |
| Button supports localization | locale.ts, activity.server.ts |
| Button redirects to configured URL | Checkout.tsx |
| Same order is not pushed twice | idempotency.server.ts, idempotency.test.ts |
| Calls are logged | logger.server.ts |
| Third-party signing is correct | signature.server.ts, signature.test.ts |
| Sensitive data is not exposed to frontend | backend-only config and sanitized logs |
Out of Scope For This Phase
- Order Status Page.
orders/paidwebhook fallback.- App admin UI for managing Metaobjects.
- Actual deployment.
- Real third-party API validation.
- Real Shopify test order validation.