# 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 `$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. 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: ```bash 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: ```text function-613wh0ez.myshopify.com ``` ## Environment Copy `.env.example` to `.env` and fill Shopify credentials: ```env 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`: ```json { "en": "Claim Your Reward", "zh-CN": "领取奖励", "zh-TW": "領取獎勵", "de": "Belohnung erhalten", "fr": "Réclamer votre récompense" } ``` Optional setup script: ```bash 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: ```text POST /api/action/activity/check ``` Headers: ```text Authorization: Bearer Content-Type: application/json ``` Request: ```json { "orderId": "gid://shopify/Order/1234567890", "orderNumber": "#1001", "locale": "zh-CN" } ``` Success response: ```json { "success": true, "redirectUrl": "https://activity.example.com", "buttonText": "领取奖励" } ``` Failure response: ```json { "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: ```text 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: ```env 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: ```env THIRD_PARTY_MODE=live ``` ## Database Development uses SQLite: ```env 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: ```text extensions/thank-you-activity ``` Target: ```text purchase.thank-you.block.render ``` Capabilities: ```toml [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: ```bash 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: 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.