dreame-action/README.md

293 lines
8.0 KiB
Markdown

# 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:
```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
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`:
```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 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:
```text
POST /api/action/activity/check
```
Headers:
```text
Authorization: Bearer <checkout session token>
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`.
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.
## 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/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.