Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a6d204f
01a0234b - fix(buy): show and send exact CHF with rappen (#926)
TaprootFreak Aug 21, 2026
8321754
Merge pull request #927 from RealUnitCH/staging
TaprootFreak Aug 21, 2026
568f6a4
01a02915 - Live RealUnit geo-filter table in the handbook (#930)
TaprootFreak Aug 22, 2026
f491ac0
01a02915 - Geo-filter DE/EN names and downloads (#932)
TaprootFreak Aug 22, 2026
519bdbb
fix(buy): keep the amount field editable — quote the Rappen-exact pay…
Blume1977 Aug 24, 2026
81aaf68
Merge pull request #931 from RealUnitCH/staging
TaprootFreak Aug 24, 2026
a438df3
01a03350 - Show exact buy charge and skip duplicate Maestro on ready …
TaprootFreak Aug 24, 2026
42dea71
01a033fb - test(buy): pin EUR quotes to the EUR settlement IBAN (#938)
TaprootFreak Aug 24, 2026
4a71a59
01a0344d - fix(buy): hide FormatException on 502 and pin retry-only g…
TaprootFreak Aug 24, 2026
eeb8812
01a033b5 - Buy CTA: quote max amount and thousands grouping (#936)
TaprootFreak Aug 24, 2026
892f909
01a033fb - Default display currency from account residence, not local…
TaprootFreak Aug 25, 2026
9e75d18
Merge pull request #940 from RealUnitCH/staging
TaprootFreak Aug 25, 2026
cc96b44
01a03ced - Delay send-process start until the route slide finishes (#…
TaprootFreakAI Aug 26, 2026
d0174ce
01a03ced - Wait for incoming slides before starting process work (#946)
TaprootFreakAI Aug 26, 2026
05a4a50
Merge pull request #945 from RealUnitCH/staging
TaprootFreak Aug 26, 2026
172f329
01a03dfc - Send: receipt timeout is success, no raw viem (#947)
TaprootFreakAI Aug 26, 2026
cd4cbe9
01a03e98 - Don't double-push send/pay after a live QR scan (#948)
TaprootFreakAI Aug 26, 2026
ce58eee
Merge pull request #949 from RealUnitCH/staging
TaprootFreak Aug 26, 2026
31fc2fc
01a03951 - BitBox sell: faucet then unsigned swap and deposit (#950)
TaprootFreakAI Aug 27, 2026
19ca0a1
01a042cc - Golden for the collapsed all-zero portfolio chart (#953)
TaprootFreakAI Aug 27, 2026
6f75fe5
01a042cc - Keep portfolio chart visible when holdings are zero (#951)
TaprootFreakAI Aug 27, 2026
b7841e9
01a042cc - Handbook test-customer portfolio charts (#954)
TaprootFreakAI Aug 28, 2026
209c85a
01a042cc - Bump handbook mail-preview count to 28 (#956)
TaprootFreakAI Aug 28, 2026
ce45560
Merge pull request #952 from RealUnitCH/staging
TaprootFreak Aug 28, 2026
d5b760b
01a04768 - Move Send to the receive QR screen and add send/pay info (…
TaprootFreakAI Aug 28, 2026
222aaef
Merge pull request #957 from RealUnitCH/staging
TaprootFreak Aug 28, 2026
e7ccd98
01a04768 - Retry Maestro driver hangs and freeze handbook persona dat…
TaprootFreakAI Aug 29, 2026
ecc5131
Merge pull request #959 from RealUnitCH/staging
TaprootFreak Aug 29, 2026
8fb18a2
01a04768 - Retry Sentry symbol upload on store deploy (#960)
TaprootFreakAI Aug 29, 2026
1c4cf03
Merge pull request #961 from RealUnitCH/staging
TaprootFreak Aug 29, 2026
36a4c16
01a04768 - Do not block Play Store on stuck Sentry assemble (#962)
TaprootFreakAI Aug 30, 2026
fd01f27
Merge pull request #963 from RealUnitCH/staging
TaprootFreak Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/handbook-build-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ jobs:
set -euo pipefail
bash scripts/assemble-handbook-screenshots.sh /tmp/handbook-shots
count=$(ls -1 /tmp/handbook-shots/*.png | wc -l | tr -d ' ')
if [ "$count" != "278" ]; then
echo "expected 278 screenshots, got $count" >&2
if [ "$count" != "284" ]; then
echo "expected 284 screenshots, got $count" >&2
exit 1
fi

Expand Down Expand Up @@ -173,11 +173,11 @@ jobs:
exit 1
fi

# Screenshots dir must contain all 278 PNGs assembled from Goldens.
# Screenshots dir must contain all 284 PNGs assembled from Goldens.
# Hit one of them through the auth gate to verify wiring end-to-end.
# Mix of the original 01-61 range and the 62-269 batch (every Golden
# baseline) so a regression in either half surfaces here.
for name in 01-welcome 11-dashboard 26-terms 35-dashboard-with-balance 46-buy-kyc-required 52-sell-unknown-error 53-buy-payment-details 61-kyc-registration-tax-tin-error 62-welcome-page-android 219-settings-security-page-default 268-phone-number-field-default 269-dashboard-insider-unlocked; do
for name in 01-welcome 11-dashboard 26-terms 35-dashboard-with-balance 46-buy-kyc-required 52-sell-unknown-error 53-buy-payment-details 61-kyc-registration-tax-tin-error 62-welcome-page-android 219-settings-security-page-default 268-phone-number-field-default 269-dashboard-insider-unlocked 270-handbook-persona-dca 275-handbook-persona-exit-1j; do
code=$(curl -s -o /dev/null -w '%{http_code}' -u "${HANDBOOK_USER:-x}:${HANDBOOK_PASS:-x}" "http://127.0.0.1:8080/screenshots/${name}.png")
# 200 (auth happens to match) or 401 (auth fails but file exists)
# both prove the file is on disk. 404 means it was not assembled.
Expand Down
23 changes: 12 additions & 11 deletions .github/workflows/handbook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,20 @@ jobs:
env:
API_DIR: _api-checkout
HANDLEBARS_PREFIX: _handlebars-only
# Exact expected mail-preview file count: 24 mails + 1 index = 25.
# `-ne` below makes ANY drift (drop OR addition) fail the build —
# adding a new mail upstream therefore requires an explicit edit
# here, which is the whole point: a silently-added (or silently-
# removed) mail must never reach the handbook image undetected.
# When you legitimately add/remove a mail in
# DFXswiss/api/scripts/generate-realunit-previews.js, bump this
# number in the SAME PR as the api-side change-PR (or its
# follow-up). The exact count is computed as:
# N(add() calls in the generator)
# Exact expected mail-preview file count: 24 standalone add() mails
# + 3 pendingTypes expansions + 1 index = 28. `-ne` below makes ANY
# drift (drop OR addition) fail the build — adding a new mail
# upstream therefore requires an explicit edit here, which is the
# whole point: a silently-added (or silently-removed) mail must
# never reach the handbook image undetected. When you legitimately
# add/remove a mail in DFXswiss/api/scripts/generate-realunit-previews.js,
# bump this number in the SAME PR as the api-side change-PR (or
# its follow-up). The exact count is computed as:
# N(standalone add() calls in the generator; the loop template
# is not counted here)
# + N(pendingTypes entries the for-loop expands)
# + 1 (00_index.html)
EXPECTED_HTML_COUNT: 25
EXPECTED_HTML_COUNT: 28
run: |
set -euo pipefail

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ jobs:
- run: dart run tool/generate_localization.dart
- run: dart run tool/generate_release_info.dart
- run: flutter pub run build_runner build
- run: bash scripts/run-handbook-flows.sh --matcher-self-test
- run: flutter analyze
# Excludes the `golden` tag: visual-regression tests live under
# `test/goldens/` and are validated on the self-hosted runner
Expand Down
41 changes: 32 additions & 9 deletions .github/workflows/tier3-handbook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,24 @@ name: Tier 3 — Handbook flows
# the existing Analyze & Test job. Reviewers opt-in by labelling
# the PR. The label gate is the cost control, not the branch
# filter — stacked PRs against integration branches need the same
# opt-in option.
# opt-in option. Types are `opened` / `synchronize` / `reopened` /
# `labeled` / `unlabeled`. `ready_for_review` is omitted on purpose:
# a labelled draft that is later marked ready must not start a
# second macOS run — the labelled run already covers that SHA.
# (Contrast `pull-request.yaml`, where draft-skip means becoming
# ready is the first real run and `ready_for_review` belongs there.)
# * `push: develop` always runs — post-merge verification is the
# authoritative source of truth for "did the handbook flows survive
# this merge", independent of any PR state. Same reasoning as
# `pull-request.yaml`.
# * `workflow_dispatch` for manual catch-up runs and a re-run path when
# the macOS image flips an Xcode default and we need to verify the fix
# before re-arming the gate. Always runs (label gate doesn't apply).
# * `labeled` / `unlabeled` keep the trigger surface in sync with the
# `tier3:full` opt-in: adding the label after the PR opened starts a
# run; removing it cancels via the concurrency block below.
# * `labeled` / `unlabeled` stay in `types` so GitHub delivers the
# opt-in events. Adding `tier3:full` starts a run (job `if` matches
# that label name). Removing it does not start a run and does not
# cancel an in-flight one — labeled events use a unique concurrency
# group, so an unlabeled skip cannot stop the flow already running.
#
# Concurrency:
# Same pattern as `pull-request.yaml`: group by `pull_request.number`
Expand All @@ -36,7 +43,10 @@ name: Tier 3 — Handbook flows
# get their own `ci-<workflow>-label-<run_id>` group so a label toggle
# does NOT kill an already-running ~15-minute flow execution — the user
# experience of "I added the label, the previous run died" is worse
# than the cost of one extra runner-minute.
# than the cost of one extra runner-minute. That unique group is also
# why omitting `ready_for_review` matters: a later ready event would
# join the PR-number group and would not cancel the in-flight labeled
# run, so both would race on macos-latest.
#
# Locale: scripts/run-handbook-flows.sh pins the booted simulator to de_CH
# so German handbook assertions pass on `macos-latest` runners (default
Expand Down Expand Up @@ -66,8 +76,11 @@ name: Tier 3 — Handbook flows
# as a safety net for that residual crash class, retrying the
# driver hang/death class against both the CLI tee-log and
# `--debug-output` maestro.log (`IOSDriverTimeoutException`,
# ConnectException on :7001, Connection refused/reset) —
# assertion failures without those patterns are never retried.
# ConnectException on :7001, Connection refused/reset,
# UnknownFailure HTTP 500 on :7001/deviceInfo). A hung JVM after
# that 500 is killed per attempt (`MAESTRO_ATTEMPT_TIMEOUT_SEC`)
# so the 60-minute job does not sit until GitHub cancels it.
# Assertion failures without those patterns are never retried.
# The post-mortem block (lsof / ps / simctl log + Maestro
# `--debug-output`) captures failure state so future regressions
# can be diagnosed forensically. The CI-hardening track that landed
Expand Down Expand Up @@ -110,7 +123,7 @@ on:
# covered by `push: develop` on the same SHA — see the longer
# rationale in `pull-request.yaml`).
branches-ignore: [main]
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
types: [opened, synchronize, reopened, labeled, unlabeled]

concurrency:
group: >-
Expand All @@ -131,9 +144,19 @@ jobs:
# Drafts pass through unconditionally on push (they cannot push, so
# the only way here is via a labelled PR) — no extra `draft == false`
# guard needed.
# `labeled` must match the opt-in label itself: with only `contains(...)`,
# adding any other label on an already-labelled PR would start a second
# macos-latest run (labeled events use a unique concurrency group and
# would not cancel the in-flight one). `unlabeled` is skipped here —
# the unique group also means it cannot cancel an in-flight flow.
if: >-
github.event_name != 'pull_request'
|| contains(github.event.pull_request.labels.*.name, 'tier3:full')
|| (github.event.action == 'labeled' && github.event.label.name == 'tier3:full')
|| (
github.event.action != 'labeled'
&& github.event.action != 'unlabeled'
&& contains(github.event.pull_request.labels.*.name, 'tier3:full')
)
runs-on: macos-latest
# ~12 min setup (Flutter + tooling, build, sim erase/boot/install) plus
# ~26-33 min for the 26 handbook flows — each flow restarts the XCUITest
Expand Down
19 changes: 19 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ The app must stay fully usable on every **standard phone** we support (smallest

New sticky-CTA UI without `ScrollableActionsLayout` + matrix entry is a **blocking** review finding.

## Scanner navigation — CRITICAL

[`QrScannerView`](lib/widgets/scanner/qr_scanner_view.dart) / `MobileScanner` forwards **every camera frame** while a barcode is in view — it is not a one-shot capture.

**Navigation rule**

- Navigating from a scan result in the same turn as `cubit.reset()` is **forbidden**: it drops the cubit's "already decoded" / Valid guard, so the next frame pushes a second copy of the next route. The user ends up on two stacked amount (or quote) screens and cannot leave cleanly.
- Production contract: every `QrScannerView` consumer uses [`pushThenRearm`](lib/widgets/scanner/push_then_rearm.dart) to push the next route. Rearm / `reset` runs after `Route.completed` (the outgoing animation is gone) or immediately if the push throws — **never** in the same listener turn. In this app a scanner that does not push a route is not a valid consumer — add it to the catalog with `pushThenRearm` or do not construct `QrScannerView`.
- Cubit `onCodeDetected` must ignore further detections while the decoded/valid state is held. That guard is **necessary and not sufficient** — without `pushThenRearm`, an early `reset()` makes it a no-op.

**Test rule (gate for this bug class)**

- Catalog: [`test/helper/scanner_navigation_catalog.dart`](test/helper/scanner_navigation_catalog.dart).
- Self-test: [`scanner_navigation_catalog_test.dart`](test/helper/scanner_navigation_catalog_test.dart) — every catalogued production + regression path exists; production files contain `QrScannerView(` **and** `pushThenRearm(`; regression files fire `BarcodeCapture` and assert the destination `findsOne`; **discovery** walks `lib/` and fails if any `QrScannerView(` consumer is missing from the catalog (skipping the widget definition file itself).
- Each surface has a real-cubit widget test that fires **two** `BarcodeCapture`s and expects the destination `findsOne`, then pop, then a third capture is accepted again. Mocking the cubit / `whenListen` of a single Valid/Decoded state cannot catch this bug class.
- Details and PR checklist: [`docs/testing.md`](docs/testing.md) § *Scanner navigation*.

New `QrScannerView` consumer without catalog entry + `pushThenRearm` + double-capture test is a **blocking** review finding.

## Branch Flow

Three branches participate in the release lane:
Expand Down
58 changes: 41 additions & 17 deletions android/fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,33 @@ def upload_sentry_symbols(marketing_version:, version_code:, symbols_path:, dart
UI.user_error!("Sentry upload: Dart obfuscation map not found at #{dart_symbol_map_path}")
end

sh("dart", "run", "sentry_dart_plugin",
"--sentry-define=symbols_path=#{symbols_path}",
"--sentry-define=dart_symbol_map_path=#{dart_symbol_map_path}"
)
# Self-hosted Sentry can return "Not all chunks available for
# assembling" after "Nothing to upload" (files already on the
# server, assemble stuck). That pair continues. Other plugin
# errors retry up to max_attempts, then fail the lane.
max_attempts = 3
attempt = 0
begin
attempt += 1
sh("dart", "run", "sentry_dart_plugin",
"--sentry-define=symbols_path=#{symbols_path}",
"--sentry-define=dart_symbol_map_path=#{dart_symbol_map_path}"
)
rescue FastlaneCore::Interface::FastlaneShellError => e
stuck = e.message.include?("Not all chunks available for assembling") &&
e.message.include?("Nothing to upload")
if stuck
# Files are already on the server; waiting for assemble loops on
# incomplete chunks. Do not fail the lane after that.
UI.important("Sentry already has these files but assemble is stuck; continuing")
elsif attempt >= max_attempts
raise
else
UI.important("Sentry symbol upload failed (#{attempt}/#{max_attempts}), retrying in 20s")
sleep 20
retry
end
end
end
end

Expand Down Expand Up @@ -133,23 +156,11 @@ platform :android do
end
end

upload_sentry_symbols(
marketing_version: marketing_version,
version_code: version_code,
symbols_path: "build/debug_info/appbundle",
dart_symbol_map_path: "build/appbundle-obfuscation-map.json"
)
upload_sentry_symbols(
marketing_version: marketing_version,
version_code: version_code,
symbols_path: "build/debug_info/apk",
dart_symbol_map_path: "build/apk-obfuscation-map.json"
)

# Upload the binary + changelog only. The changelog is tied to this
# build's version code (taken from the AAB); the listing metadata and
# images are pushed by the dedicated call below, keeping the two
# concerns separate and avoiding a duplicate metadata upload.
# Play Store first: a stuck Sentry assemble must not block testers.
upload_to_play_store(
track: release_track,
aab: aab_path,
Expand All @@ -173,6 +184,19 @@ platform :android do
skip_upload_screenshots: false,
track: release_track
)

upload_sentry_symbols(
marketing_version: marketing_version,
version_code: version_code,
symbols_path: "build/debug_info/appbundle",
dart_symbol_map_path: "build/appbundle-obfuscation-map.json"
)
upload_sentry_symbols(
marketing_version: marketing_version,
version_code: version_code,
symbols_path: "build/debug_info/apk",
dart_symbol_map_path: "build/apk-obfuscation-map.json"
)
end

desc "Upload Play Store listing (metadata + screenshots) without a binary"
Expand Down
10 changes: 8 additions & 2 deletions assets/languages/strings_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"blockchain": "Blockchain",
"buy": "Kaufen",
"buyBackToMain": "Zurück zum Hauptbereich",
"buyChargedAmount": "Genau belastet",
"buyMaxAmount": "Maximalbetrag: ${amount} ${currency}",
"buyMinAmount": "Mindestbetrag: ${amount} ${currency}",
"buyPaymentConfirm": "Jetzt verbindlich kaufen",
"buyPaymentDetailsTitle": "Zahlungsdetails",
Expand Down Expand Up @@ -183,6 +185,8 @@
"payFailureQuoteExpired": "Das Zahlungsangebot ist abgelaufen. Bitte scannen Sie den Code erneut.",
"payFailureSignatureUnsupported": "Diese Wallet kann keine Transaktionen signieren. Wechseln Sie zu einer Software- oder BitBox-Wallet.",
"payFailureTitle": "Zahlung fehlgeschlagen",
"payInfoBody": "Die Bezahlfunktion kann an allen OpenCryptoPay-Standorten genutzt werden.\n\nDer Tausch erfolgt in zwei Schritten. Zuerst wird REALU in ZCHF getauscht. ZCHF ist ein CHF-Stablecoin, der anschliessend an den Zahlungsempfänger übertragen wird.\n\nREALU ist ein Wertpapier und wird nicht für die Bezahlung selbst verwendet.",
"payInfoTitle": "Bevor Sie bezahlen",
"paymentInformationFailed": "Beim Abrufen der Zahlungsinformationen ist ein Fehler aufgetreten.",
"paymentInformationFailedDescription": "Bitte versuchen Sie es später erneut. Wenn der Fehler weiterhin besteht, wenden Sie sich an unseren Support.",
"payoutAccountAdd": "Auszahlungskonto hinzufügen",
Expand Down Expand Up @@ -350,6 +354,8 @@
"sendFailureSignatureCancelled": "Die Signatur wurde abgebrochen. Ihre REALU bleiben unangetastet.",
"sendFailureSignatureUnsupported": "Diese Wallet kann keine gaslosen Überweisungen signieren. Wechseln Sie zu einer Software-Wallet, um REALU zu senden.",
"sendFailureTitle": "Überweisung fehlgeschlagen",
"sendInfoBody": "Sie können REALU nur an Wallet-Adressen senden, die bei RealUnit als Aktionärin oder Aktionär registriert sind. Nur so kann der Empfänger sein Stimmrecht ausüben.\n\nDurch die Ausführung dieser Transaktion wird das Aktienbuch aktualisiert.",
"sendInfoTitle": "Bevor Sie senden",
"sending": "Wird gesendet",
"sendPaste": "Einfügen",
"sendPreparing": "Überweisung wird vorbereitet",
Expand All @@ -359,7 +365,7 @@
"sendRecipientManualHint": "Scannen Sie einen Wallet-QR-Code oder fügen Sie die Empfängeradresse unten ein.",
"sendRecipientTitle": "Empfänger scannen",
"sendShares": "${shares} REALU",
"sendSigning": "Bestätigen Sie die Überweisung in Ihrer Wallet",
"sendSigning": "Überweisung wird gesendet",
"sendSuccess": "Überweisung gesendet",
"sendSuccessDescription": "Ihre REALU sind auf dem Weg zum Empfänger.",
"setNationalityFailed": "Ihre Staatsangehörigkeit konnte nicht gesetzt werden:\n${message}",
Expand Down Expand Up @@ -465,4 +471,4 @@
"youPay": "Sie bezahlen",
"youReceive": "Sie erhalten",
"youSell": "Sie verkaufen"
}
}
10 changes: 8 additions & 2 deletions assets/languages/strings_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"blockchain": "Blockchain",
"buy": "Buy",
"buyBackToMain": "Back to main area",
"buyChargedAmount": "Exact charge",
"buyMaxAmount": "Maximum amount: ${amount} ${currency}",
"buyMinAmount": "Minimum amount: ${amount} ${currency}",
"buyPaymentConfirm": "Buy now bindingly",
"buyPaymentDetailsTitle": "Payment details",
Expand Down Expand Up @@ -183,6 +185,8 @@
"payFailureQuoteExpired": "The payment quote expired. Please scan the code again.",
"payFailureSignatureUnsupported": "This wallet cannot sign transactions. Switch to a software or BitBox wallet.",
"payFailureTitle": "Payment failed",
"payInfoBody": "You can use the payment function at all OpenCryptoPay locations.\n\nThe exchange happens in two steps. First, REALU is swapped for ZCHF. ZCHF is a Swiss franc stablecoin, which is then transferred to the payment recipient.\n\nREALU is a security and is not used for the payment itself.",
"payInfoTitle": "Before you pay",
"paymentInformationFailed": "An error occurred while getting the payment information.",
"paymentInformationFailedDescription": "Please try again later. If the error persists, contact our support team.",
"payoutAccountAdd": "Add payout account",
Expand Down Expand Up @@ -350,6 +354,8 @@
"sendFailureSignatureCancelled": "The signature was cancelled. Your REALU is untouched.",
"sendFailureSignatureUnsupported": "This wallet cannot sign gasless transfers. Switch to a software wallet to send REALU.",
"sendFailureTitle": "Transfer failed",
"sendInfoBody": "You can only send REALU to wallet addresses that are registered as RealUnit shareholders. This is required so the recipient can exercise their voting rights.\n\nCompleting this transfer updates the share register.",
"sendInfoTitle": "Before you send",
"sending": "Sending",
"sendPaste": "Paste",
"sendPreparing": "Preparing transfer",
Expand All @@ -359,7 +365,7 @@
"sendRecipientManualHint": "Scan a wallet QR code, or paste the recipient address below.",
"sendRecipientTitle": "Scan recipient",
"sendShares": "${shares} REALU",
"sendSigning": "Confirm the transfer in your wallet",
"sendSigning": "Sending the transfer",
"sendSuccess": "Transfer sent",
"sendSuccessDescription": "Your REALU is on its way to the recipient.",
"setNationalityFailed": "Could not set your nationality:\n${message}",
Expand Down Expand Up @@ -465,4 +471,4 @@
"youPay": "You pay",
"youReceive": "You receive",
"youSell": "You sell"
}
}
Loading
Loading