dreame-action/README.md

10 KiB

dreame-action

Shopify custom app scaffold for the Dreame action 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 $app: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.

Business failures return { "success": false }; route-level validation errors can include an error object and non-2xx status. The extension renders nothing for either case 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-action"
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
}

Route-level validation failures include an error code and use the matching HTTP status:

{
  "success": false,
  "error": {
    "code": "ORDER_ID_REQUIRED",
    "message": "orderId is required."
  }
}

All route responses include Cache-Control: no-store, max-age=0. Authenticated POST and OPTIONS responses are wrapped with Shopify's checkout CORS helper; fallback CORS is only used when authentication itself fails before the helper is available. 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.
  • Live success criteria can be overridden through Metaobject fields status_success_code, status_active_required, and push_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 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.

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.
  • test/activity-route.test.ts: checkout auth/CORS/cache HTTP contract, invalid shop context, missing order ID, auth failure handling.
  • test/checkout-extension.test.ts: extension app URL normalization, session-token POST request, non-2xx and invalid JSON fallback behavior.

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. Deploy the app-owned $app:dreame_activity definition and upsert the dreame-activity-config entry.
  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.