Shift member communication to app channel and remove notify-email flow
This commit is contained in:
parent
65744ef700
commit
d58c92ad55
@ -64,6 +64,7 @@ docs/
|
|||||||
marketplace.examples.md
|
marketplace.examples.md
|
||||||
handoff/
|
handoff/
|
||||||
membership-backend-checklist.md
|
membership-backend-checklist.md
|
||||||
|
marketplace-backend-checklist.md
|
||||||
schemas/
|
schemas/
|
||||||
offer.v1.schema.json
|
offer.v1.schema.json
|
||||||
entitlement.v1.schema.json
|
entitlement.v1.schema.json
|
||||||
|
|||||||
@ -150,38 +150,7 @@ Error (`400` tx mismatch):
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5) `POST /secret/notify`
|
## 5) `GET /secret/membership/status`
|
||||||
|
|
||||||
Request:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"email": "user@example.com",
|
|
||||||
"designation_code": "0217073045482",
|
|
||||||
"designation_token": "0217-0730-4548-2",
|
|
||||||
"wallet": "0x3ea6cbf98d23e2cf7b6f4f9bb1fb4f50b710f2d5",
|
|
||||||
"locale": "en"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Success (`200`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "saved"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Error (`422` invalid email):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error": "invalid_email",
|
|
||||||
"message": "Email format is invalid."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6) `GET /secret/membership/status`
|
|
||||||
|
|
||||||
Request by wallet:
|
Request by wallet:
|
||||||
|
|
||||||
|
|||||||
@ -92,27 +92,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/MembershipStatusResponse'
|
$ref: '#/components/schemas/MembershipStatusResponse'
|
||||||
/secret/notify:
|
|
||||||
post:
|
|
||||||
summary: Save optional notification email
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/NotifyRequest'
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Notification saved
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
required: [status]
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
enum: [saved]
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
WalletIntentRequest:
|
WalletIntentRequest:
|
||||||
@ -265,19 +244,3 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
designation_code:
|
designation_code:
|
||||||
type: string
|
type: string
|
||||||
NotifyRequest:
|
|
||||||
type: object
|
|
||||||
required: [email, designation_code, wallet, locale]
|
|
||||||
properties:
|
|
||||||
email:
|
|
||||||
type: string
|
|
||||||
format: email
|
|
||||||
designation_code:
|
|
||||||
type: string
|
|
||||||
designation_token:
|
|
||||||
type: string
|
|
||||||
wallet:
|
|
||||||
type: string
|
|
||||||
pattern: '^0x[a-fA-F0-9]{40}$'
|
|
||||||
locale:
|
|
||||||
type: string
|
|
||||||
|
|||||||
@ -15,7 +15,6 @@ This matrix defines deterministic fail-closed behavior and user-facing outcomes.
|
|||||||
| Confirm | Amount mismatch | Quote/tx comparator | Reject confirm | "Transaction does not match quote." |
|
| Confirm | Amount mismatch | Quote/tx comparator | Reject confirm | "Transaction does not match quote." |
|
||||||
| Confirm | Recipient mismatch | Quote/tx comparator | Reject confirm | "Destination contract mismatch." |
|
| Confirm | Recipient mismatch | Quote/tx comparator | Reject confirm | "Destination contract mismatch." |
|
||||||
| Confirm | Node unavailable | RPC health | Fail closed | "Unable to confirm transaction. Purchase stays blocked." |
|
| Confirm | Node unavailable | RPC health | Fail closed | "Unable to confirm transaction. Purchase stays blocked." |
|
||||||
| Notify | Invalid email | Input validation | Reject notify | "Invalid email format." |
|
|
||||||
| Checkout | No membership | Gate check | Block purchase | "Membership required." |
|
| Checkout | No membership | Gate check | Block purchase | "Membership required." |
|
||||||
| Checkout | Membership suspended/revoked | Gate check | Block purchase | "Membership inactive. Contact support." |
|
| Checkout | Membership suspended/revoked | Gate check | Block purchase | "Membership inactive. Contact support." |
|
||||||
| Activation | Entitlement not active | Gate check | Block runtime | "License inactive. Activation blocked." |
|
| Activation | Entitlement not active | Gate check | Block runtime | "License inactive. Activation blocked." |
|
||||||
|
|||||||
65
docs/handoff/marketplace-backend-checklist.md
Normal file
65
docs/handoff/marketplace-backend-checklist.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# Backend Handoff Checklist: Marketplace Checkout and Entitlements
|
||||||
|
|
||||||
|
This checklist maps store behavior to required marketplace backend implementation.
|
||||||
|
|
||||||
|
## Required Endpoints
|
||||||
|
|
||||||
|
1. `GET /marketplace/offers`
|
||||||
|
2. `GET /marketplace/offers/{offer_id}`
|
||||||
|
3. `POST /marketplace/checkout/quote`
|
||||||
|
4. `POST /marketplace/checkout/confirm`
|
||||||
|
5. `GET /marketplace/entitlements?wallet=...`
|
||||||
|
|
||||||
|
## Required Gate Behavior
|
||||||
|
|
||||||
|
1. Quote endpoint must deny checkout when membership is not active.
|
||||||
|
2. Confirm endpoint must verify quote policy hash and tx match.
|
||||||
|
3. Entitlement state must default fail-closed for unknown values.
|
||||||
|
|
||||||
|
## Store Dependency Mapping
|
||||||
|
|
||||||
|
1. Store catalog requests `/marketplace/offers` (fallback to local JSON until live).
|
||||||
|
2. Store quote action posts selected `offer_id` and wallet.
|
||||||
|
3. Store expects quote payload with tx execution fields.
|
||||||
|
4. Store confirm path expects `entitlement_active` response.
|
||||||
|
|
||||||
|
## Quote Response Requirements
|
||||||
|
|
||||||
|
1. `quote_id`
|
||||||
|
2. `wallet`
|
||||||
|
3. `offer_id`
|
||||||
|
4. `currency`
|
||||||
|
5. `amount` or `amount_atomic + decimals`
|
||||||
|
6. `policy_hash`
|
||||||
|
7. `expires_at`
|
||||||
|
8. `tx` execution object or equivalent fields
|
||||||
|
|
||||||
|
## Confirm Response Requirements
|
||||||
|
|
||||||
|
1. `status = entitlement_active`
|
||||||
|
2. `entitlement_id`
|
||||||
|
3. `offer_id`
|
||||||
|
4. `wallet`
|
||||||
|
5. `tx_hash`
|
||||||
|
6. `policy_hash`
|
||||||
|
7. `activated_at`
|
||||||
|
|
||||||
|
## Persistence Requirements
|
||||||
|
|
||||||
|
1. Quote record with policy hash and expiry.
|
||||||
|
2. Confirm record linked to tx hash and entitlement id.
|
||||||
|
3. Entitlement lifecycle state with immutable issued event evidence.
|
||||||
|
|
||||||
|
## Security Requirements
|
||||||
|
|
||||||
|
1. Membership gate check on quote and confirm paths.
|
||||||
|
2. Quote TTL enforcement.
|
||||||
|
3. Tx chain, amount, and destination validation.
|
||||||
|
4. Idempotent confirm handling for repeated tx hash submissions.
|
||||||
|
|
||||||
|
## Done Criteria
|
||||||
|
|
||||||
|
1. Store can request quotes for active members only.
|
||||||
|
2. Confirm endpoint issues active entitlements deterministically.
|
||||||
|
3. Entitlement listing endpoint returns current state records.
|
||||||
|
4. API matches `docs/api/marketplace.openapi.yaml`.
|
||||||
@ -8,7 +8,7 @@ This checklist maps current web behavior to required backend implementation.
|
|||||||
2. `POST /secret/wallet/verify`
|
2. `POST /secret/wallet/verify`
|
||||||
3. `POST /secret/membership/quote`
|
3. `POST /secret/membership/quote`
|
||||||
4. `POST /secret/membership/confirm`
|
4. `POST /secret/membership/confirm`
|
||||||
5. `POST /secret/notify`
|
5. `GET /secret/membership/status`
|
||||||
|
|
||||||
## Web Behavior Dependency
|
## Web Behavior Dependency
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ The landing page currently executes these actions in order:
|
|||||||
5. Request membership quote.
|
5. Request membership quote.
|
||||||
6. Send wallet transaction (`eth_sendTransaction`) using returned tx params.
|
6. Send wallet transaction (`eth_sendTransaction`) using returned tx params.
|
||||||
7. Confirm membership by tx hash.
|
7. Confirm membership by tx hash.
|
||||||
8. Show acknowledged state and optional notify form.
|
8. Show acknowledged state and app download links.
|
||||||
|
|
||||||
If any endpoint is missing, flow fails closed and shows status error.
|
If any endpoint is missing, flow fails closed and shows status error.
|
||||||
|
|
||||||
@ -69,15 +69,12 @@ Must return:
|
|||||||
3. `display_token`
|
3. `display_token`
|
||||||
4. `tx_hash`
|
4. `tx_hash`
|
||||||
|
|
||||||
## Notify
|
## Membership Status
|
||||||
|
|
||||||
Must accept:
|
Must return:
|
||||||
|
|
||||||
1. `email`
|
1. `status` (`active|none|suspended|revoked|unknown`)
|
||||||
2. `designation_code`
|
2. selector echo (`wallet` and/or `designation_code`)
|
||||||
3. `designation_token`
|
|
||||||
4. `wallet`
|
|
||||||
5. `locale`
|
|
||||||
|
|
||||||
## Security Requirements
|
## Security Requirements
|
||||||
|
|
||||||
@ -97,7 +94,7 @@ Persist at minimum:
|
|||||||
3. intent fields and verification time
|
3. intent fields and verification time
|
||||||
4. quote fields and expiry
|
4. quote fields and expiry
|
||||||
5. membership tx hash and activation timestamp
|
5. membership tx hash and activation timestamp
|
||||||
6. notification email link metadata
|
6. membership status resolution fields for wallet/designation lookups
|
||||||
|
|
||||||
## Observability Requirements
|
## Observability Requirements
|
||||||
|
|
||||||
@ -108,7 +105,7 @@ Persist at minimum:
|
|||||||
- verify success/fail
|
- verify success/fail
|
||||||
- quote success/fail
|
- quote success/fail
|
||||||
- confirm success/fail
|
- confirm success/fail
|
||||||
- notify success/fail
|
- membership status lookups success/fail
|
||||||
|
|
||||||
## Done Criteria
|
## Done Criteria
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
5. Updated secret system architecture to wallet-first membership flow.
|
5. Updated secret system architecture to wallet-first membership flow.
|
||||||
- Replaced SMS/email-first assumptions with wallet signature + membership mint sequence.
|
- Replaced SMS/email-first assumptions with wallet signature + membership mint sequence.
|
||||||
- Added membership-gate framing (`membership required`, `membership != product license`).
|
- Added membership-gate framing (`membership required`, `membership != product license`).
|
||||||
- Kept notification email optional and post-membership.
|
- Set post-membership value delivery to platform download and app-native communication.
|
||||||
|
|
||||||
6. Privacy policy hardening.
|
6. Privacy policy hardening.
|
||||||
- Removed over-broad ad-tech assumptions.
|
- Removed over-broad ad-tech assumptions.
|
||||||
|
|||||||
@ -73,7 +73,7 @@ This roadmap is intentionally step-based and dependency-ordered. No timeline com
|
|||||||
## Step 11: Harden Compliance and Policy Surfaces
|
## Step 11: Harden Compliance and Policy Surfaces
|
||||||
|
|
||||||
1. Terms clearly separate membership rights from license rights.
|
1. Terms clearly separate membership rights from license rights.
|
||||||
2. Privacy describes wallet/signature processing and optional notifications.
|
2. Privacy describes wallet/signature processing and app-native member communication.
|
||||||
3. Public copy avoids investment framing and speculative claims.
|
3. Public copy avoids investment framing and speculative claims.
|
||||||
|
|
||||||
## Step 12: Operational Hardening
|
## Step 12: Operational Hardening
|
||||||
@ -89,4 +89,3 @@ This roadmap is intentionally step-based and dependency-ordered. No timeline com
|
|||||||
- visit -> signature -> membership mint -> first entitlement purchase.
|
- visit -> signature -> membership mint -> first entitlement purchase.
|
||||||
2. Tune copy, wallet guidance, and pricing policy using deterministic metrics.
|
2. Tune copy, wallet guidance, and pricing policy using deterministic metrics.
|
||||||
3. Expand issuer ecosystem only after quality and support controls are stable.
|
3. Expand issuer ecosystem only after quality and support controls are stable.
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,7 @@ Status key:
|
|||||||
Implemented now:
|
Implemented now:
|
||||||
|
|
||||||
1. Wallet-first landing flow with intent + signature + membership tx hooks.
|
1. Wallet-first landing flow with intent + signature + membership tx hooks.
|
||||||
2. Optional notification link step.
|
2. Post-mint app delivery step (`download your platform`) for immediate member value.
|
||||||
3. Membership-gated architecture spec.
|
3. Membership-gated architecture spec.
|
||||||
4. Step-based roadmap without timelines.
|
4. Step-based roadmap without timelines.
|
||||||
5. Frozen v1 schemas and examples.
|
5. Frozen v1 schemas and examples.
|
||||||
|
|||||||
@ -10,7 +10,7 @@ The Edut onboarding flow is deterministic and wallet-first:
|
|||||||
4. Server verifies signature and creates/updates designation record.
|
4. Server verifies signature and creates/updates designation record.
|
||||||
5. Visitor mints paid EDUT membership on-chain (intent proof).
|
5. Visitor mints paid EDUT membership on-chain (intent proof).
|
||||||
6. Server confirms transaction and marks membership active.
|
6. Server confirms transaction and marks membership active.
|
||||||
7. Visitor may optionally add notification email for launch updates.
|
7. Visitor downloads the EDUT platform app from post-mint success links.
|
||||||
|
|
||||||
This flow is the pre-launch identity and commerce envelope. It is not a throwaway waitlist.
|
This flow is the pre-launch identity and commerce envelope. It is not a throwaway waitlist.
|
||||||
|
|
||||||
@ -29,7 +29,8 @@ This flow is the pre-launch identity and commerce envelope. It is not a throwawa
|
|||||||
11. Wallet submits membership mint transaction on Base.
|
11. Wallet submits membership mint transaction on Base.
|
||||||
12. Page confirms via `POST /secret/membership/confirm` and/or status poll.
|
12. Page confirms via `POST /secret/membership/confirm` and/or status poll.
|
||||||
13. UI shows `acknowledged · {token}` when membership is active.
|
13. UI shows `acknowledged · {token}` when membership is active.
|
||||||
14. Optional: user submits email via `POST /secret/notify`.
|
14. Post-mint success state presents `download your platform` links (Desktop/iOS/Android).
|
||||||
|
15. Member opens the app, signs in with the same wallet, and receives platform updates through app notifications.
|
||||||
|
|
||||||
Privacy and Terms links bypass flow and navigate normally.
|
Privacy and Terms links bypass flow and navigate normally.
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ Landing page -> POST /secret/membership/quote
|
|||||||
Wallet sends paid mint tx on Base
|
Wallet sends paid mint tx on Base
|
||||||
Landing page -> POST /secret/membership/confirm
|
Landing page -> POST /secret/membership/confirm
|
||||||
Membership active -> acknowledged state
|
Membership active -> acknowledged state
|
||||||
Optional notification email -> POST /secret/notify
|
Post-mint success -> app download links (Desktop/iOS/Android)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Core Commerce Rule
|
## Core Commerce Rule
|
||||||
@ -51,6 +52,7 @@ Optional notification email -> POST /secret/notify
|
|||||||
1. Membership is required to purchase marketplace offers.
|
1. Membership is required to purchase marketplace offers.
|
||||||
2. Membership is not a product/module license.
|
2. Membership is not a product/module license.
|
||||||
3. Offer-specific licenses/entitlements are purchased separately.
|
3. Offer-specific licenses/entitlements are purchased separately.
|
||||||
|
4. Membership purchase delivers initial platform access (download entry point) immediately after activation.
|
||||||
|
|
||||||
## Infrastructure
|
## Infrastructure
|
||||||
|
|
||||||
@ -61,18 +63,13 @@ Optional notification email -> POST /secret/notify
|
|||||||
| API | `api.edut.ai/secret/wallet/verify` | Verify signature and bind wallet identity |
|
| API | `api.edut.ai/secret/wallet/verify` | Verify signature and bind wallet identity |
|
||||||
| API | `api.edut.ai/secret/membership/quote` | Return current payable membership quote |
|
| API | `api.edut.ai/secret/membership/quote` | Return current payable membership quote |
|
||||||
| API | `api.edut.ai/secret/membership/confirm` | Confirm membership tx and activation state |
|
| API | `api.edut.ai/secret/membership/confirm` | Confirm membership tx and activation state |
|
||||||
| API | `api.edut.ai/secret/notify` | Store optional launch notification email |
|
|
||||||
| Chain | Base | Membership mint settlement and evidence |
|
| Chain | Base | Membership mint settlement and evidence |
|
||||||
| Database | `/var/lib/edut/secrets.db` | Durable designation + membership + notification state |
|
| Database | `/var/lib/edut/secrets.db` | Durable designation + membership state |
|
||||||
|
|
||||||
## Deterministic State Machine
|
## Deterministic State Machine
|
||||||
|
|
||||||
`pending_signature` -> `signature_verified` -> `pending_membership_mint` -> `membership_active`
|
`pending_signature` -> `signature_verified` -> `pending_membership_mint` -> `membership_active`
|
||||||
|
|
||||||
Optional post-activation state:
|
|
||||||
|
|
||||||
- `notification_linked` (email saved)
|
|
||||||
|
|
||||||
Additional side states:
|
Additional side states:
|
||||||
|
|
||||||
- `intent_expired`
|
- `intent_expired`
|
||||||
@ -237,29 +234,6 @@ Response:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5) Optional Notification Link
|
|
||||||
|
|
||||||
#### `POST /secret/notify`
|
|
||||||
|
|
||||||
Request JSON:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"email": "user@example.com",
|
|
||||||
"designation_code": "0217073045482",
|
|
||||||
"designation_token": "0217-0730-4548-2",
|
|
||||||
"wallet": "0xabc123...",
|
|
||||||
"locale": "en"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Behavior:
|
|
||||||
|
|
||||||
1. Validate email format and normalize case.
|
|
||||||
2. Bind email to active designation/membership record.
|
|
||||||
3. Keep idempotent updates by designation or wallet.
|
|
||||||
4. Record timestamp and source IP for abuse controls.
|
|
||||||
|
|
||||||
## Security Controls
|
## Security Controls
|
||||||
|
|
||||||
1. Intent TTL and one-time nonce consumption.
|
1. Intent TTL and one-time nonce consumption.
|
||||||
@ -295,8 +269,6 @@ CREATE TABLE IF NOT EXISTS designations (
|
|||||||
membership_quote_expires_at DATETIME,
|
membership_quote_expires_at DATETIME,
|
||||||
membership_tx_hash TEXT,
|
membership_tx_hash TEXT,
|
||||||
membership_activated_at DATETIME,
|
membership_activated_at DATETIME,
|
||||||
notification_email TEXT,
|
|
||||||
notification_linked_at DATETIME,
|
|
||||||
origin TEXT,
|
origin TEXT,
|
||||||
locale TEXT,
|
locale TEXT,
|
||||||
created_at DATETIME DEFAULT (datetime('now'))
|
created_at DATETIME DEFAULT (datetime('now'))
|
||||||
@ -333,9 +305,6 @@ location /secret/membership/confirm {
|
|||||||
proxy_pass http://127.0.0.1:9091;
|
proxy_pass http://127.0.0.1:9091;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /secret/notify {
|
|
||||||
proxy_pass http://127.0.0.1:9091;
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|||||||
@ -20,7 +20,8 @@ The site should feel precise and established while avoiding disclosure of privat
|
|||||||
6. They mint EDUT membership as a paid on-chain commitment.
|
6. They mint EDUT membership as a paid on-chain commitment.
|
||||||
7. Membership confirmation binds durable account state to the wallet.
|
7. Membership confirmation binds durable account state to the wallet.
|
||||||
8. Final state shows `acknowledged · {token}`.
|
8. Final state shows `acknowledged · {token}`.
|
||||||
9. Optional `notify me` captures email for launch updates.
|
9. Post-mint success reveals `download your platform` links immediately.
|
||||||
|
10. Member communication occurs through the app after wallet sign-in (native notification channel).
|
||||||
|
|
||||||
The flow should feel controlled and ambient, not like a conventional signup form.
|
The flow should feel controlled and ambient, not like a conventional signup form.
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ The flow should feel controlled and ambient, not like a conventional signup form
|
|||||||
|
|
||||||
- `edut.ai`: primary public surface and business identity.
|
- `edut.ai`: primary public surface and business identity.
|
||||||
- `edut.dev`: developer-facing domain (same landing for now).
|
- `edut.dev`: developer-facing domain (same landing for now).
|
||||||
- `api.edut.ai`: API endpoints for wallet intent/verify, membership quote/confirm, and optional notification linking.
|
- `api.edut.ai`: API endpoints for wallet intent/verify and membership quote/confirm.
|
||||||
- `/privacy` and `/terms`: legal pages (English authoritative).
|
- `/privacy` and `/terms`: legal pages (English authoritative).
|
||||||
|
|
||||||
## Messaging Boundaries
|
## Messaging Boundaries
|
||||||
@ -89,7 +90,7 @@ The designation and membership flow is the pre-launch identity envelope:
|
|||||||
|
|
||||||
- Factor 1: cryptographic wallet possession (signature proof)
|
- Factor 1: cryptographic wallet possession (signature proof)
|
||||||
- Factor 2: economic commitment (paid membership mint)
|
- Factor 2: economic commitment (paid membership mint)
|
||||||
- Optional channel: notification email binding
|
- Delivery channel: app install + wallet sign-in
|
||||||
- Shared binding: one server-generated designation record (`code` + `auth_token`)
|
- Shared binding: one server-generated designation record (`code` + `auth_token`)
|
||||||
|
|
||||||
When launch activation opens, this record becomes the continuity bridge for deployment onboarding and activation messaging.
|
When launch activation opens, this record becomes the continuity bridge for deployment onboarding and activation messaging.
|
||||||
|
|||||||
@ -263,26 +263,12 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notify-form {
|
.download-links {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
justify-content: center;
|
||||||
}
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
.notify-input {
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 1px solid #606264;
|
|
||||||
background: transparent;
|
|
||||||
color: #3a3d42;
|
|
||||||
font-family: 'IBM Plex Mono', 'Courier New', Courier, monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
padding: 5px 3px;
|
|
||||||
min-width: 220px;
|
|
||||||
}
|
|
||||||
.notify-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-bottom-color: #2c2c2c;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
@ -416,7 +402,7 @@
|
|||||||
<div class="flow-ui" id="flow-ui">
|
<div class="flow-ui" id="flow-ui">
|
||||||
<button id="continue-action" class="ghost-action localizable flow-hidden" data-i18n="continue_label" type="button">continue</button>
|
<button id="continue-action" class="ghost-action localizable flow-hidden" data-i18n="continue_label" type="button">continue</button>
|
||||||
<div id="wallet-panel" class="flow-panel flow-hidden">
|
<div id="wallet-panel" class="flow-panel flow-hidden">
|
||||||
<p class="flow-line localizable" data-i18n="wallet_intro">receive your designation token</p>
|
<p class="flow-line localizable" data-i18n="wallet_intro">mint your membership</p>
|
||||||
<p class="flow-line subtle localizable" data-i18n="wallet_fact_no_tx">No transaction. Signature only.</p>
|
<p class="flow-line subtle localizable" data-i18n="wallet_fact_no_tx">No transaction. Signature only.</p>
|
||||||
<p class="flow-line subtle localizable" data-i18n="wallet_fact_seed">Never share your seed phrase.</p>
|
<p class="flow-line subtle localizable" data-i18n="wallet_fact_seed">Never share your seed phrase.</p>
|
||||||
<div class="flow-actions">
|
<div class="flow-actions">
|
||||||
@ -429,12 +415,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="flow-status" class="flow-status flow-hidden" aria-live="polite"></div>
|
<div id="flow-status" class="flow-status flow-hidden" aria-live="polite"></div>
|
||||||
<div id="notify-panel" class="notify-panel flow-hidden">
|
<div id="delivery-panel" class="notify-panel flow-hidden">
|
||||||
<button id="notify-open" class="flow-link localizable" data-i18n="notify_me" type="button">notify me</button>
|
<p class="flow-line localizable" data-i18n="download_heading">download your platform</p>
|
||||||
<form id="notify-form" class="notify-form flow-hidden" autocomplete="on">
|
<div class="download-links">
|
||||||
<input id="notify-email" class="notify-input localizable" data-i18n-placeholder="notify_placeholder" type="email" placeholder="email" required />
|
<a id="download-desktop" class="flow-link localizable" data-i18n="download_desktop" href="/downloads/desktop">desktop</a>
|
||||||
<button id="notify-submit" class="flow-link localizable" data-i18n="notify_submit" type="submit">save</button>
|
<a id="download-ios" class="flow-link localizable" data-i18n="download_ios" href="/downloads/ios">iOS</a>
|
||||||
</form>
|
<a id="download-android" class="flow-link localizable" data-i18n="download_android" href="/downloads/android">android</a>
|
||||||
|
</div>
|
||||||
|
<p class="flow-line subtle localizable" data-i18n="app_notifications_note">member updates are delivered inside the app after wallet sign-in.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="sr-only localizable" id="interaction-hint" data-i18n="interaction_hint">Click anywhere on the page to begin your access request.</span>
|
<span class="sr-only localizable" id="interaction-hint" data-i18n="interaction_hint">Click anywhere on the page to begin your access request.</span>
|
||||||
@ -490,10 +478,7 @@ const walletHave = document.getElementById('wallet-have');
|
|||||||
const walletNeed = document.getElementById('wallet-need');
|
const walletNeed = document.getElementById('wallet-need');
|
||||||
const walletInstallHelp = document.getElementById('wallet-install-help');
|
const walletInstallHelp = document.getElementById('wallet-install-help');
|
||||||
const flowStatus = document.getElementById('flow-status');
|
const flowStatus = document.getElementById('flow-status');
|
||||||
const notifyPanel = document.getElementById('notify-panel');
|
const deliveryPanel = document.getElementById('delivery-panel');
|
||||||
const notifyOpen = document.getElementById('notify-open');
|
|
||||||
const notifyForm = document.getElementById('notify-form');
|
|
||||||
const notifyEmail = document.getElementById('notify-email');
|
|
||||||
const privacyLink = document.getElementById('privacyLink');
|
const privacyLink = document.getElementById('privacyLink');
|
||||||
const termsLink = document.getElementById('termsLink');
|
const termsLink = document.getElementById('termsLink');
|
||||||
|
|
||||||
@ -564,6 +549,12 @@ function renderAcknowledged(state) {
|
|||||||
el.classList.add('visible');
|
el.classList.add('visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showPostMintPanel() {
|
||||||
|
hideElement(continueAction);
|
||||||
|
hideElement(walletPanel);
|
||||||
|
showElement(deliveryPanel);
|
||||||
|
}
|
||||||
|
|
||||||
function setFlowStatus(key, fallback, isError) {
|
function setFlowStatus(key, fallback, isError) {
|
||||||
flowStatus.textContent = t(key, fallback);
|
flowStatus.textContent = t(key, fallback);
|
||||||
flowStatus.classList.toggle('error', !!isError);
|
flowStatus.classList.toggle('error', !!isError);
|
||||||
@ -683,7 +674,10 @@ function applyLocale(bundle, lang) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const saved = getStoredAcknowledgement();
|
const saved = getStoredAcknowledgement();
|
||||||
if (saved) renderAcknowledged(saved);
|
if (saved) {
|
||||||
|
renderAcknowledged(saved);
|
||||||
|
showPostMintPanel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializeLocale() {
|
async function initializeLocale() {
|
||||||
@ -833,9 +827,7 @@ async function startWalletFlow() {
|
|||||||
};
|
};
|
||||||
saveAcknowledgement(ackState);
|
saveAcknowledgement(ackState);
|
||||||
renderAcknowledged(ackState);
|
renderAcknowledged(ackState);
|
||||||
hideElement(continueAction);
|
showPostMintPanel();
|
||||||
hideElement(walletPanel);
|
|
||||||
showElement(notifyPanel);
|
|
||||||
setFlowStatus('membership_active', 'Membership active. Designation acknowledged.', false);
|
setFlowStatus('membership_active', 'Membership active. Designation acknowledged.', false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err && err.message ? err.message : 'Wallet flow failed.';
|
const message = err && err.message ? err.message : 'Wallet flow failed.';
|
||||||
@ -843,26 +835,6 @@ async function startWalletFlow() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitNotifyEmail(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const email = notifyEmail.value.trim();
|
|
||||||
if (!email) return;
|
|
||||||
const ack = getStoredAcknowledgement();
|
|
||||||
try {
|
|
||||||
await postJSON('/secret/notify', {
|
|
||||||
email,
|
|
||||||
designation_code: ack && ack.code ? ack.code : null,
|
|
||||||
designation_token: ack && ack.token ? ack.token : null,
|
|
||||||
wallet: ack && ack.wallet ? ack.wallet : null,
|
|
||||||
locale: localStorage.getItem('edut_lang') || 'en',
|
|
||||||
});
|
|
||||||
setFlowStatus('notify_saved', 'Notification saved.', false);
|
|
||||||
notifyForm.classList.add('flow-hidden');
|
|
||||||
} catch (err) {
|
|
||||||
setFlowStatus('notify_failed', 'Could not save notification preference.', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFirstInteractionReveal() {
|
function onFirstInteractionReveal() {
|
||||||
if (flowState.firstInteraction || getStoredAcknowledgement()) return;
|
if (flowState.firstInteraction || getStoredAcknowledgement()) return;
|
||||||
flowState.firstInteraction = true;
|
flowState.firstInteraction = true;
|
||||||
@ -880,10 +852,6 @@ continueAction.addEventListener('click', () => {
|
|||||||
});
|
});
|
||||||
walletHave.addEventListener('click', startWalletFlow);
|
walletHave.addEventListener('click', startWalletFlow);
|
||||||
walletNeed.addEventListener('click', openWalletInstallHints);
|
walletNeed.addEventListener('click', openWalletInstallHints);
|
||||||
notifyOpen.addEventListener('click', () => {
|
|
||||||
notifyForm.classList.toggle('flow-hidden');
|
|
||||||
});
|
|
||||||
notifyForm.addEventListener('submit', submitNotifyEmail);
|
|
||||||
|
|
||||||
// Keyboard support: Enter/Space triggers same as click
|
// Keyboard support: Enter/Space triggers same as click
|
||||||
document.body.addEventListener('keydown', (e) => {
|
document.body.addEventListener('keydown', (e) => {
|
||||||
|
|||||||
@ -157,7 +157,7 @@
|
|||||||
|
|
||||||
<h2>Information We Collect</h2>
|
<h2>Information We Collect</h2>
|
||||||
|
|
||||||
<p><strong>Information you provide directly.</strong> When you interact with our sites — including connecting a wallet, signing a designation message, entering optional notification details, contacting support, or completing a transaction — we may collect your wallet address, signed message payloads, email address, contact details, billing details, message content, and any other information you choose to provide.</p>
|
<p><strong>Information you provide directly.</strong> When you interact with our sites — including connecting a wallet, signing a designation message, downloading platform software, contacting support, or completing a transaction — we may collect your wallet address, signed message payloads, contact details, billing details, message content, and any other information you choose to provide.</p>
|
||||||
|
|
||||||
<p><strong>Information collected automatically.</strong> When you visit our sites, we may collect technical and usage information such as IP address, browser type, operating system, referring URLs, pages viewed, timestamps, locale preferences, and interaction events. This information is collected through server logs, local storage, cookies, and similar technologies.</p>
|
<p><strong>Information collected automatically.</strong> When you visit our sites, we may collect technical and usage information such as IP address, browser type, operating system, referring URLs, pages viewed, timestamps, locale preferences, and interaction events. This information is collected through server logs, local storage, cookies, and similar technologies.</p>
|
||||||
|
|
||||||
@ -169,7 +169,7 @@
|
|||||||
|
|
||||||
<p>We may use information we collect to operate, maintain, and improve our websites, products, and services; process transactions and send related confirmations, invoices, and receipts; communicate with you regarding security, operations, product updates, and support; analyze reliability and usage trends; detect and prevent fraud, abuse, and unauthorized access; comply with legal obligations; and enforce our agreements.</p>
|
<p>We may use information we collect to operate, maintain, and improve our websites, products, and services; process transactions and send related confirmations, invoices, and receipts; communicate with you regarding security, operations, product updates, and support; analyze reliability and usage trends; detect and prevent fraud, abuse, and unauthorized access; comply with legal obligations; and enforce our agreements.</p>
|
||||||
|
|
||||||
<p><strong>Verified-channel purpose.</strong> Where designation workflows are used, we process cryptographic signature data to confirm wallet control, prevent automated abuse, bind protocol records to a stable identity anchor, and maintain auditable activation continuity. If you provide an optional notification email, we use it to deliver launch and operational notices tied to your designation.</p>
|
<p><strong>Verified-channel purpose.</strong> Where designation workflows are used, we process cryptographic signature data to confirm wallet control, prevent automated abuse, bind protocol records to a stable identity anchor, and maintain auditable activation continuity. Member operational notices are delivered through platform software after wallet sign-in.</p>
|
||||||
|
|
||||||
<h2>Blockchain and Wallet Data</h2>
|
<h2>Blockchain and Wallet Data</h2>
|
||||||
<p>Wallet addresses and related signature metadata may be processed to verify cryptographic intent and establish designation records. Public blockchain networks are independently operated systems; if designation or licensing records are written on-chain, related transaction data may be publicly visible and immutable by design. We do not control third-party blockchain explorers or wallet software.</p>
|
<p>Wallet addresses and related signature metadata may be processed to verify cryptographic intent and establish designation records. Public blockchain networks are independently operated systems; if designation or licensing records are written on-chain, related transaction data may be publicly visible and immutable by design. We do not control third-party blockchain explorers or wallet software.</p>
|
||||||
@ -178,7 +178,7 @@
|
|||||||
|
|
||||||
<p><strong>Service providers.</strong> We may share your information with third-party providers that support hosting, infrastructure, email delivery, wallet connectivity infrastructure, support tooling, analytics, and payment processing. These providers are contractually obligated to process information only for authorized service purposes.</p>
|
<p><strong>Service providers.</strong> We may share your information with third-party providers that support hosting, infrastructure, email delivery, wallet connectivity infrastructure, support tooling, analytics, and payment processing. These providers are contractually obligated to process information only for authorized service purposes.</p>
|
||||||
|
|
||||||
<p><strong>Communications providers.</strong> We may use communications processors for operational email delivery and response handling. These providers process contact information and message metadata as necessary to transmit and verify communications.</p>
|
<p><strong>Communications providers.</strong> We may use communications processors for operational email support handling and app-notification infrastructure. These providers process message metadata as necessary to transmit and verify communications.</p>
|
||||||
|
|
||||||
<p><strong>Legal requirements.</strong> We may disclose your information if required to do so by law, regulation, legal process, or governmental request, or when we believe disclosure is necessary to protect our rights, your safety, or the safety of others, or to investigate fraud or respond to a government request.</p>
|
<p><strong>Legal requirements.</strong> We may disclose your information if required to do so by law, regulation, legal process, or governmental request, or when we believe disclosure is necessary to protect our rights, your safety, or the safety of others, or to investigate fraud or respond to a government request.</p>
|
||||||
|
|
||||||
|
|||||||
@ -51,5 +51,10 @@
|
|||||||
"notify_placeholder": "البريد الإلكتروني",
|
"notify_placeholder": "البريد الإلكتروني",
|
||||||
"notify_submit": "حفظ",
|
"notify_submit": "حفظ",
|
||||||
"notify_saved": "تم حفظ الإشعار.",
|
"notify_saved": "تم حفظ الإشعار.",
|
||||||
"notify_failed": "تعذر حفظ تفضيل الإشعار."
|
"notify_failed": "تعذر حفظ تفضيل الإشعار.",
|
||||||
|
"download_heading": "نزّل منصتك",
|
||||||
|
"download_desktop": "سطح المكتب",
|
||||||
|
"download_ios": "iOS",
|
||||||
|
"download_android": "أندرويد",
|
||||||
|
"app_notifications_note": "تصل تحديثات الأعضاء داخل التطبيق بعد تسجيل الدخول بالمحفظة."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,5 +51,10 @@
|
|||||||
"notify_placeholder": "E-Mail",
|
"notify_placeholder": "E-Mail",
|
||||||
"notify_submit": "speichern",
|
"notify_submit": "speichern",
|
||||||
"notify_saved": "Benachrichtigung gespeichert.",
|
"notify_saved": "Benachrichtigung gespeichert.",
|
||||||
"notify_failed": "Benachrichtigungseinstellung konnte nicht gespeichert werden."
|
"notify_failed": "Benachrichtigungseinstellung konnte nicht gespeichert werden.",
|
||||||
|
"download_heading": "lade deine plattform herunter",
|
||||||
|
"download_desktop": "desktop",
|
||||||
|
"download_ios": "iOS",
|
||||||
|
"download_android": "android",
|
||||||
|
"app_notifications_note": "mitglieder-updates werden in der app nach wallet-anmeldung zugestellt."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,5 +51,10 @@
|
|||||||
"notify_placeholder": "email",
|
"notify_placeholder": "email",
|
||||||
"notify_submit": "save",
|
"notify_submit": "save",
|
||||||
"notify_saved": "Notification saved.",
|
"notify_saved": "Notification saved.",
|
||||||
"notify_failed": "Could not save notification preference."
|
"notify_failed": "Could not save notification preference.",
|
||||||
|
"download_heading": "download your platform",
|
||||||
|
"download_desktop": "desktop",
|
||||||
|
"download_ios": "iOS",
|
||||||
|
"download_android": "android",
|
||||||
|
"app_notifications_note": "member updates are delivered inside the app after wallet sign-in."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,5 +51,10 @@
|
|||||||
"notify_placeholder": "correo",
|
"notify_placeholder": "correo",
|
||||||
"notify_submit": "guardar",
|
"notify_submit": "guardar",
|
||||||
"notify_saved": "Notificación guardada.",
|
"notify_saved": "Notificación guardada.",
|
||||||
"notify_failed": "No se pudo guardar la preferencia de notificación."
|
"notify_failed": "No se pudo guardar la preferencia de notificación.",
|
||||||
|
"download_heading": "descarga tu plataforma",
|
||||||
|
"download_desktop": "escritorio",
|
||||||
|
"download_ios": "iOS",
|
||||||
|
"download_android": "android",
|
||||||
|
"app_notifications_note": "las actualizaciones para miembros se entregan dentro de la app tras iniciar sesion con wallet."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,5 +51,10 @@
|
|||||||
"notify_placeholder": "e-mail",
|
"notify_placeholder": "e-mail",
|
||||||
"notify_submit": "enregistrer",
|
"notify_submit": "enregistrer",
|
||||||
"notify_saved": "Notification enregistree.",
|
"notify_saved": "Notification enregistree.",
|
||||||
"notify_failed": "Impossible denregistrer la preference de notification."
|
"notify_failed": "Impossible denregistrer la preference de notification.",
|
||||||
|
"download_heading": "telechargez votre plateforme",
|
||||||
|
"download_desktop": "bureau",
|
||||||
|
"download_ios": "iOS",
|
||||||
|
"download_android": "android",
|
||||||
|
"app_notifications_note": "les mises a jour membres sont envoyees dans lapp apres connexion du wallet."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,5 +51,10 @@
|
|||||||
"notify_placeholder": "אימייל",
|
"notify_placeholder": "אימייל",
|
||||||
"notify_submit": "שמור",
|
"notify_submit": "שמור",
|
||||||
"notify_saved": "ההתראה נשמרה.",
|
"notify_saved": "ההתראה נשמרה.",
|
||||||
"notify_failed": "לא ניתן לשמור את העדפת ההתראה."
|
"notify_failed": "לא ניתן לשמור את העדפת ההתראה.",
|
||||||
|
"download_heading": "הורד את הפלטפורמה שלך",
|
||||||
|
"download_desktop": "דסקטופ",
|
||||||
|
"download_ios": "iOS",
|
||||||
|
"download_android": "אנדרואיד",
|
||||||
|
"app_notifications_note": "עדכוני חברים נמסרים בתוך האפליקציה לאחר התחברות בארנק."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,5 +51,10 @@
|
|||||||
"notify_placeholder": "ईमेल",
|
"notify_placeholder": "ईमेल",
|
||||||
"notify_submit": "सहेजें",
|
"notify_submit": "सहेजें",
|
||||||
"notify_saved": "सूचना सहेज ली गई।",
|
"notify_saved": "सूचना सहेज ली गई।",
|
||||||
"notify_failed": "सूचना वरीयता सहेजी नहीं जा सकी।"
|
"notify_failed": "सूचना वरीयता सहेजी नहीं जा सकी।",
|
||||||
|
"download_heading": "अपना प्लेटफ़ॉर्म डाउनलोड करें",
|
||||||
|
"download_desktop": "डेस्कटॉप",
|
||||||
|
"download_ios": "iOS",
|
||||||
|
"download_android": "एंड्रॉइड",
|
||||||
|
"app_notifications_note": "सदस्य अपडेट वॉलेट साइन-इन के बाद ऐप के अंदर दिए जाते हैं।"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,5 +51,10 @@
|
|||||||
"notify_placeholder": "メール",
|
"notify_placeholder": "メール",
|
||||||
"notify_submit": "保存",
|
"notify_submit": "保存",
|
||||||
"notify_saved": "通知設定を保存しました。",
|
"notify_saved": "通知設定を保存しました。",
|
||||||
"notify_failed": "通知設定を保存できませんでした。"
|
"notify_failed": "通知設定を保存できませんでした。",
|
||||||
|
"download_heading": "プラットフォームをダウンロード",
|
||||||
|
"download_desktop": "デスクトップ",
|
||||||
|
"download_ios": "iOS",
|
||||||
|
"download_android": "android",
|
||||||
|
"app_notifications_note": "メンバー向け更新はウォレットサインイン後にアプリ内で配信されます。"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,5 +51,10 @@
|
|||||||
"notify_placeholder": "이메일",
|
"notify_placeholder": "이메일",
|
||||||
"notify_submit": "저장",
|
"notify_submit": "저장",
|
||||||
"notify_saved": "알림 설정이 저장되었습니다.",
|
"notify_saved": "알림 설정이 저장되었습니다.",
|
||||||
"notify_failed": "알림 설정을 저장하지 못했습니다."
|
"notify_failed": "알림 설정을 저장하지 못했습니다.",
|
||||||
|
"download_heading": "플랫폼 다운로드",
|
||||||
|
"download_desktop": "데스크톱",
|
||||||
|
"download_ios": "iOS",
|
||||||
|
"download_android": "android",
|
||||||
|
"app_notifications_note": "회원 업데이트는 지갑 로그인 후 앱 안에서 전달됩니다."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,5 +51,10 @@
|
|||||||
"notify_placeholder": "e-mail",
|
"notify_placeholder": "e-mail",
|
||||||
"notify_submit": "salvar",
|
"notify_submit": "salvar",
|
||||||
"notify_saved": "Notificação salva.",
|
"notify_saved": "Notificação salva.",
|
||||||
"notify_failed": "Não foi possível salvar a preferência de notificação."
|
"notify_failed": "Não foi possível salvar a preferência de notificação.",
|
||||||
|
"download_heading": "baixe sua plataforma",
|
||||||
|
"download_desktop": "desktop",
|
||||||
|
"download_ios": "iOS",
|
||||||
|
"download_android": "android",
|
||||||
|
"app_notifications_note": "as atualizacoes para membros sao entregues no app apos login da wallet."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,5 +51,10 @@
|
|||||||
"notify_placeholder": "email",
|
"notify_placeholder": "email",
|
||||||
"notify_submit": "сохранить",
|
"notify_submit": "сохранить",
|
||||||
"notify_saved": "Уведомление сохранено.",
|
"notify_saved": "Уведомление сохранено.",
|
||||||
"notify_failed": "Не удалось сохранить настройки уведомлений."
|
"notify_failed": "Не удалось сохранить настройки уведомлений.",
|
||||||
|
"download_heading": "скачайте свою платформу",
|
||||||
|
"download_desktop": "десктоп",
|
||||||
|
"download_ios": "iOS",
|
||||||
|
"download_android": "android",
|
||||||
|
"app_notifications_note": "обновления для участников доставляются в приложении после входа через кошелек."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,5 +51,10 @@
|
|||||||
"notify_placeholder": "邮箱",
|
"notify_placeholder": "邮箱",
|
||||||
"notify_submit": "保存",
|
"notify_submit": "保存",
|
||||||
"notify_saved": "通知已保存。",
|
"notify_saved": "通知已保存。",
|
||||||
"notify_failed": "无法保存通知偏好。"
|
"notify_failed": "无法保存通知偏好。",
|
||||||
|
"download_heading": "下载你的平台",
|
||||||
|
"download_desktop": "桌面版",
|
||||||
|
"download_ios": "iOS",
|
||||||
|
"download_android": "安卓",
|
||||||
|
"app_notifications_note": "会员更新会在钱包登录后通过应用内发送。"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user