dreame-action/README.md

8.0 KiB

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

  1. Customer lands on Shopify Thank You Page.
  2. Checkout UI Extension target purchase.thank-you.block.render loads.
  3. Extension gets orderId, orderNumber, locale, and a Shopify session token.
  4. Extension calls POST /api/action/activity/check.
  5. Backend validates the checkout session token, queries order data through Admin GraphQL, reads the dreame_activity Metaobject config, checks order-level idempotency, calls Dreame status API, then calls Dreame push API.
  6. On success, backend returns success, redirectUrl, and localized buttonText.
  7. Extension renders a button and opens redirectUrl in 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
DATABASE_URL="file:./dev.sqlite"
THIRD_PARTY_MODE=mock
THIRD_PARTY_TIMEOUT_MS=5000
LOG_RETENTION_DAYS=7
METAOBJECT_TYPE=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

Create one Metaobject definition:

  • Type: dreame_activity
  • Suggested handle: dreame-activity-config
  • Storefront access: NONE

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
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 requires write scopes such as write_metaobjects and write_metaobject_definitions. It is not needed if the Metaobject is created manually in Shopify Admin.

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-id
  • dreame-api-timestamp
  • dreame-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=0 and data.status=true.
  • Push API returns code=0.

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 on shop + 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.

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/activity-flow.test.ts: inactive status, non-zero status code, successful push payload shape, push failure, missing email, missing customer ID.

Deployment Steps

This task does not deploy. For production deployment later:

  1. Provision Postgres.
  2. Set production environment variables.
  3. Update shopify.app.toml URLs.
  4. Run Prisma migration against production.
  5. Run shopify app deploy.
  6. Publish the Checkout UI Extension.
  7. Configure the extension app_url setting.
  8. Create the dreame_activity Metaobject in the production shop.
  9. Switch THIRD_PARTY_MODE=live.
  10. Run end-to-end validation with real test orders and third-party test endpoints.

Security Notes

  • api_secret is 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_DAYS days.

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/paid webhook fallback.
  • App admin UI for managing Metaobjects.
  • Actual deployment.
  • Real third-party API validation.
  • Real Shopify test order validation.