diff --git a/PLAN.md b/PLAN.md index afd6114..bf7c203 100644 --- a/PLAN.md +++ b/PLAN.md @@ -635,9 +635,13 @@ impl) + the VK bridge (`vkRewardedReady` / `vkShowRewarded`), the backend `Credi order-less, idempotent on a client nonce, floored by the caps, payout from config `rewarded_payout_chips` default 0 = off), the `wallet.reward` edge op returning the updated wallet (with `reward_chips` gating the "watch for chips" CTA), and a **contour test stub** (`VITE_ADS_STUB` → -a toast instead of a real ad; prod always real). A temporary diagnostic logs the raw VK `data` so we -confirm on the contour exactly what VK returns (harden to signature-verify if it carries one). The -legacy `paid_account` / `hint_balance` drop (D31) and the **interstitial** are the next slice. +a toast instead of a real ad; prod always real). A temporary diagnostic confirmed on the contour that +VK returns **only `{result:true}`** (no token/signature) — client-attested is final, no hardening +possible; the diagnostic is removed. The slice also **corrects the VK-iOS freeze to purchase-only** +(rewarded on VK-iOS earns chips, which the old blanket "spend freeze" then blocked from spending — +Apple forbids only *buying* in-app values, not spending or earning them; `vkFrozen()` now gates only +`CreateOrder`, not `spendableSources`, so VK-wallet chips spend on VK-iOS). The legacy `paid_account` +/ `hint_balance` drop (D31) and the **interstitial** are the next slice. **Goal.** VK video ads: the post-move interstitial (frequency-gated) and the rewarded video (credits chips via server verify), plus extending the existing banner suppression to diff --git a/backend/internal/payments/gate.go b/backend/internal/payments/gate.go index a136536..a3965a0 100644 --- a/backend/internal/payments/gate.go +++ b/backend/internal/payments/gate.go @@ -89,8 +89,10 @@ func NewContext(kind, subtype string) Context { // every spend/purchase and the application of any foreign origin. func (c Context) Trusted() bool { return c.Kind.Valid() } -// vkFrozen reports whether this is the VK-iOS spend freeze: VK context on the trusted iOS -// subtype. A previously bought benefit still applies there, but no spend or purchase is possible. +// vkFrozen reports whether this is the VK-iOS purchase freeze: VK context on the trusted iOS +// subtype. Apple's ToS forbids only BUYING in-app values there, so a purchase (money -> chips) is +// refused; earning chips (rewarded ads) and SPENDING chips already in the VK wallet — earned or +// bought on the same account elsewhere (e.g. VK Android) — are legal and stay allowed. func (c Context) vkFrozen() bool { return c.Kind == SourceVK && c.Subtype == SubtypeIOS } // spendPriority is the fixed draw order when several segments are spendable in one context @@ -103,11 +105,12 @@ func has(present []Source, s Source) bool { } // spendableSources returns the chip segments that may be SPENT in the context, in draw-priority -// order, restricted to the sources the account actually has (present). It is empty when the -// platform is untrusted (fail-closed) or VK-iOS (frozen): inside VK/TG only the same-named -// segment is spendable; on web/native all attached segments are, drained direct→vk→tg. +// order, restricted to the sources the account actually has (present). It is empty only when the +// platform is untrusted (fail-closed); VK-iOS is NOT excluded — the freeze is purchase-only, so +// spending VK-wallet chips there is allowed. Inside VK/TG only the same-named segment is spendable; +// on web/native all attached segments are, drained direct→vk→tg. func spendableSources(c Context, present []Source) []Source { - if !c.Trusted() || c.vkFrozen() { + if !c.Trusted() { return nil } switch c.Kind { @@ -128,10 +131,10 @@ func spendableSources(c Context, present []Source) []Source { } // applicableOrigins returns the benefit origins that APPLY in the context, in draw-priority -// order, restricted to present sources. It differs from spendableSources in one way: VK-iOS is -// NOT excluded — a benefit bought earlier still applies while spending is frozen. Inside VK/TG -// only the same-named origin applies (a foreign, e.g. direct, origin never activates inside a -// store — the compliance wall); on web/native direct+vk+tg all apply, drained direct→vk→tg. +// order, restricted to present sources. It mirrors spendableSources (both gate only on a trusted +// platform now that the VK-iOS freeze is purchase-only). Inside VK/TG only the same-named origin +// applies (a foreign, e.g. direct, origin never activates inside a store — the compliance wall); +// on web/native direct+vk+tg all apply, drained direct→vk→tg. func applicableOrigins(c Context, present []Source) []Source { if !c.Trusted() { return nil diff --git a/backend/internal/payments/gate_test.go b/backend/internal/payments/gate_test.go index 304276b..1899469 100644 --- a/backend/internal/payments/gate_test.go +++ b/backend/internal/payments/gate_test.go @@ -16,7 +16,8 @@ func TestSpendableSources(t *testing.T) { want []Source }{ {"vk android, vk present", Context{Kind: SourceVK, Subtype: "android"}, allPresent, []Source{SourceVK}}, - {"vk ios frozen", Context{Kind: SourceVK, Subtype: SubtypeIOS}, allPresent, nil}, + // VK-iOS spends its own vk segment: the freeze is purchase-only, spending is allowed. + {"vk ios spends vk", Context{Kind: SourceVK, Subtype: SubtypeIOS}, allPresent, []Source{SourceVK}}, {"vk android, vk absent", Context{Kind: SourceVK, Subtype: "android"}, []Source{SourceDirect}, nil}, {"telegram", Context{Kind: SourceTelegram, Subtype: "web"}, allPresent, []Source{SourceTelegram}}, {"telegram, tg absent", Context{Kind: SourceTelegram}, []Source{SourceVK}, nil}, @@ -41,8 +42,8 @@ func TestApplicableOrigins(t *testing.T) { present []Source want []Source }{ - // A benefit still APPLIES on VK-iOS while spending is frozen. - {"vk ios still applies", Context{Kind: SourceVK, Subtype: SubtypeIOS}, allPresent, []Source{SourceVK}}, + // A vk-origin benefit applies on VK-iOS (spending is allowed there — the freeze is purchase-only). + {"vk ios applies", Context{Kind: SourceVK, Subtype: SubtypeIOS}, allPresent, []Source{SourceVK}}, {"vk android", Context{Kind: SourceVK, Subtype: "android"}, allPresent, []Source{SourceVK}}, {"telegram", Context{Kind: SourceTelegram}, allPresent, []Source{SourceTelegram}}, {"direct all, priority", Context{Kind: SourceDirect}, allPresent, []Source{SourceDirect, SourceVK, SourceTelegram}}, diff --git a/backend/internal/payments/service_intake_test.go b/backend/internal/payments/service_intake_test.go index 11159b1..a1252a6 100644 --- a/backend/internal/payments/service_intake_test.go +++ b/backend/internal/payments/service_intake_test.go @@ -28,7 +28,7 @@ func TestCreateOrderGateRejections(t *testing.T) { cxt Context }{ {"untrusted context", Context{}}, - {"vk-ios spend freeze", Context{Kind: SourceVK, Subtype: SubtypeIOS}}, + {"vk-ios purchase freeze", Context{Kind: SourceVK, Subtype: SubtypeIOS}}, {"method segment not attached", Context{Kind: SourceTelegram}}, } for _, tc := range cases { diff --git a/backend/internal/payments/wallet_test.go b/backend/internal/payments/wallet_test.go index 8fc9aaf..e0e7150 100644 --- a/backend/internal/payments/wallet_test.go +++ b/backend/internal/payments/wallet_test.go @@ -102,10 +102,11 @@ func TestWalletSegments(t *testing.T) { if len(got.Segments) != 1 || got.Segments[0].Source != SourceVK || !got.Segments[0].Spendable { t.Errorf("vk-android wallet = %+v", got.Segments) } - // VK iOS: only vk shown, frozen (not spendable) but the balance is visible. + // VK iOS: only vk shown, and spendable — the freeze is purchase-only, so VK-wallet chips still + // spend there (only buying more chips for money is blocked). got, _ = svc.Wallet(context.Background(), id, NewContext("vk", "ios"), present) - if len(got.Segments) != 1 || got.Segments[0].Chips != 50 || got.Segments[0].Spendable { - t.Errorf("vk-ios wallet = %+v (want vk 50 frozen)", got.Segments) + if len(got.Segments) != 1 || got.Segments[0].Chips != 50 || !got.Segments[0].Spendable { + t.Errorf("vk-ios wallet = %+v (want vk 50 spendable)", got.Segments) } } diff --git a/backend/internal/server/handlers_wallet.go b/backend/internal/server/handlers_wallet.go index 0f4ba9e..89f68e7 100644 --- a/backend/internal/server/handlers_wallet.go +++ b/backend/internal/server/handlers_wallet.go @@ -175,12 +175,10 @@ func (s *Server) handleWalletBuy(c *gin.Context) { c.JSON(http.StatusOK, walletDTOFrom(view)) } -// walletRewardRequest is the POST body of a rewarded-video credit: a client nonce (the idempotency -// key for a single watched view — a retry credits once) and Diag, the raw VK ad result forwarded for -// a temporary diagnostic log while we confirm exactly what VK returns on the contour. +// walletRewardRequest is the POST body of a rewarded-video credit: a client nonce, the idempotency +// key for a single watched view (a retry credits once). type walletRewardRequest struct { Nonce string `json:"nonce"` - Diag string `json:"diag"` } // handleWalletReward credits a rewarded-video view's chips to the VK segment, client-attested and @@ -203,15 +201,6 @@ func (s *Server) handleWalletReward(c *gin.Context) { s.abortErr(c, err) return } - // Temporary diagnostic: log the raw VK ad result (bounded — untrusted client input) to learn its - // exact shape on the contour; if it carries a verifiable token we harden past client-attested. - if req.Diag != "" { - diag := req.Diag - if len(diag) > 2000 { - diag = diag[:2000] - } - s.log.Info("rewarded ad diagnostic", zap.String("account", uid.String()), zap.String("vk_data", diag)) - } outcome, err := s.payments.CreditReward(ctx, uid, cxt, present, req.Nonce) if err != nil { s.abortErr(c, err) diff --git a/docs/PAYMENTS.md b/docs/PAYMENTS.md index 572e5dd..222e08d 100644 --- a/docs/PAYMENTS.md +++ b/docs/PAYMENTS.md @@ -82,7 +82,7 @@ leaking out to the open web) is allowed. | Execution context | Spendable chip segments | Spend priority | |---------------------|-------------------------|--------------------| | Inside VK (Android) | `vk` | — | -| Inside VK (iOS) | none — frozen (view only)| — | +| Inside VK (iOS) | `vk` (purchase-frozen) | — | | Inside Telegram | `telegram` | — | | Web / native (Direct)| `direct` + `vk` + `telegram` | direct → vk → tg | @@ -90,9 +90,11 @@ leaking out to the open web) is allowed. invisible-as-spendable there. - On the web the store has no jurisdiction, so all attached segments are spendable, drained by priority direct → vk → tg. -- **VK iOS** is frozen for spending (Apple forbids spending virtual currency on digital - goods outside IAP inside VK on iOS): the balance is shown as a number, but no purchase or - spend is possible. A previously bought benefit still *applies* there. +- **VK iOS is a PURCHASE freeze, not a spend freeze.** Apple's ToS forbids only **buying** in-app + values there (for any currency) — so a purchase (money → chips) is refused. **Spending** VK-wallet + chips (earned via rewarded ads or bought on the same VK account elsewhere, e.g. VK Android) and + earning them are legal and stay allowed on VK iOS; a bought benefit also *applies* there. Only the + money-in step is blocked. The account is **single** (identities merge, one profile/friends/stats). The gate is **logical**: in a VK/TG context the server activates only the same-named segment. It rests diff --git a/docs/PAYMENTS_DECISIONS_ru.md b/docs/PAYMENTS_DECISIONS_ru.md index 4fa0775..c808915 100644 --- a/docs/PAYMENTS_DECISIONS_ru.md +++ b/docs/PAYMENTS_DECISIONS_ru.md @@ -110,6 +110,11 @@ web+PWA / native Android+iOS через Capacitor). Владелец (самоз сессии. Платформа несёт **kind** (vk/tg/direct) **+ подтип** (ios/android/web) — подтип обязателен (VK iOS заморожен). TG `initData`-валидатор уже есть (`platform/telegram/internal/initdata`). + **АМЕНД (E6, находка владельца по ToS):** VK iOS — **заморозка только ПОКУПОК** (деньги→Фишки), + а не траты. Apple запрещает там лишь **покупать** внутриигровые ценности за любую валюту; а + **тратить** Фишки VK-кошелька (заработанные rewarded-рекламой или купленные на том же VK-аккаунте + в Android) и зарабатывать их — легально и на iOS. Код: `vkFrozen()` гейтит только `CreateOrder` + (покупку), не `spendableSources` (трату). - **D18. Fail-closed:** недоверенная/неподтверждённая платформа (VK/TG-сессия без валидной подписи на старте; старая сессия без записанной платформы) → запрет трат/покупок/применения чужого origin, только просмотр. diff --git a/docs/PAYMENTS_ru.md b/docs/PAYMENTS_ru.md index 897a960..f7c73ee 100644 --- a/docs/PAYMENTS_ru.md +++ b/docs/PAYMENTS_ru.md @@ -83,7 +83,7 @@ | Контекст исполнения | Тратимые сегменты Фишек | Приоритет траты | |----------------------|---------------------------|--------------------| | Внутри VK (Android) | `vk` | — | -| Внутри VK (iOS) | нет — заморожено (только просмотр) | — | +| Внутри VK (iOS) | `vk` (заморожена покупка) | — | | Внутри Telegram | `telegram` | — | | Web / native (Direct)| `direct` + `vk` + `telegram` | direct → vk → tg | @@ -91,9 +91,11 @@ `direct`) там невидимо как тратимое. - В вебе у стора нет юрисдикции, поэтому доступны все привязанные сегменты, списываются по приоритету direct → vk → tg. -- **VK iOS** заморожен для траты (Apple запрещает тратить виртуальную валюту на цифровые - товары мимо IAP внутри VK на iOS): баланс показывается числом, но покупка/трата - невозможны. Ранее купленный бенефит там всё равно *действует*. +- **VK iOS — заморозка ПОКУПОК, а не траты.** ToS Apple запрещает там только **покупать** + внутриигровые ценности (за любую валюту) — поэтому покупка (деньги → Фишки) отклоняется. А + **тратить** Фишки VK-кошелька (заработанные рекламой или купленные на том же VK-аккаунте, напр. в + VK Android) и зарабатывать их — легально и на VK iOS разрешено; купленный бенефит там тоже + *действует*. Блокируется только шаг «деньги внутрь». Аккаунт **единый** (привязки сливаются, один профиль/друзья/статистика). Гейт **логический**: в контексте VK/TG сервер активирует только одноимённый сегмент. Держится на diff --git a/gateway/internal/backendclient/api.go b/gateway/internal/backendclient/api.go index 3c2287c..28b929b 100644 --- a/gateway/internal/backendclient/api.go +++ b/gateway/internal/backendclient/api.go @@ -384,18 +384,16 @@ type walletBuyBody struct { ProductID string `json:"product_id"` } -// walletRewardBody is the rewarded-video credit request: the per-view idempotency nonce and the raw -// VK ad result forwarded for the temporary contour diagnostic. +// walletRewardBody is the rewarded-video credit request: the per-view idempotency nonce. type walletRewardBody struct { Nonce string `json:"nonce"` - Diag string `json:"diag"` } // WalletReward credits a watched rewarded video and returns the updated wallet (client-attested + a -// backend daily cap). A reached cap or unconfigured payout surfaces as an APIError domain code. -func (c *Client) WalletReward(ctx context.Context, userID, nonce, diag string) (WalletResp, error) { +// backend daily/hourly cap). A reached cap or unconfigured payout surfaces as an APIError domain code. +func (c *Client) WalletReward(ctx context.Context, userID, nonce string) (WalletResp, error) { var out WalletResp - err := c.do(ctx, http.MethodPost, "/api/v1/user/wallet/reward", userID, "", walletRewardBody{Nonce: nonce, Diag: diag}, &out) + err := c.do(ctx, http.MethodPost, "/api/v1/user/wallet/reward", userID, "", walletRewardBody{Nonce: nonce}, &out) return out, err } diff --git a/gateway/internal/transcode/transcode.go b/gateway/internal/transcode/transcode.go index 0e3903d..e3c60e4 100644 --- a/gateway/internal/transcode/transcode.go +++ b/gateway/internal/transcode/transcode.go @@ -382,11 +382,11 @@ func walletOrderHandler(backend *backendclient.Client, minter InvoiceMinter) Han } // walletRewardHandler credits a watched rewarded video and returns the updated wallet. The nonce is -// the client's per-view idempotency key; diag carries the raw VK ad result for the contour diagnostic. +// the client's per-view idempotency key. func walletRewardHandler(backend *backendclient.Client) Handler { return func(ctx context.Context, req Request) ([]byte, error) { in := fb.GetRootAsWalletRewardRequest(req.Payload, 0) - w, err := backend.WalletReward(ctx, req.UserID, string(in.Nonce()), string(in.Diag())) + w, err := backend.WalletReward(ctx, req.UserID, string(in.Nonce())) if err != nil { return nil, err } diff --git a/pkg/fbs/scrabble.fbs b/pkg/fbs/scrabble.fbs index c8f458b..124cfb8 100644 --- a/pkg/fbs/scrabble.fbs +++ b/pkg/fbs/scrabble.fbs @@ -877,10 +877,9 @@ table WalletBuyRequest { } // WalletRewardRequest credits a watched rewarded video: nonce is the per-view idempotency key (a -// retry credits once); diag carries the raw VK ad result for a temporary contour diagnostic log. +// retry credits once). table WalletRewardRequest { nonce:string; - diag:string; } // WalletOrderRequest opens a money order to fund a chip pack: the pack to buy. diff --git a/pkg/fbs/scrabblefb/WalletRewardRequest.go b/pkg/fbs/scrabblefb/WalletRewardRequest.go index 32c2a8e..2694dde 100644 --- a/pkg/fbs/scrabblefb/WalletRewardRequest.go +++ b/pkg/fbs/scrabblefb/WalletRewardRequest.go @@ -49,23 +49,12 @@ func (rcv *WalletRewardRequest) Nonce() []byte { return nil } -func (rcv *WalletRewardRequest) Diag() []byte { - o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) - if o != 0 { - return rcv._tab.ByteVector(o + rcv._tab.Pos) - } - return nil -} - func WalletRewardRequestStart(builder *flatbuffers.Builder) { - builder.StartObject(2) + builder.StartObject(1) } func WalletRewardRequestAddNonce(builder *flatbuffers.Builder, nonce flatbuffers.UOffsetT) { builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(nonce), 0) } -func WalletRewardRequestAddDiag(builder *flatbuffers.Builder, diag flatbuffers.UOffsetT) { - builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(diag), 0) -} func WalletRewardRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() } diff --git a/ui/src/gen/fbs/scrabblefb/wallet-reward-request.ts b/ui/src/gen/fbs/scrabblefb/wallet-reward-request.ts index 4e1d9cf..5353be7 100644 --- a/ui/src/gen/fbs/scrabblefb/wallet-reward-request.ts +++ b/ui/src/gen/fbs/scrabblefb/wallet-reward-request.ts @@ -27,34 +27,22 @@ nonce(optionalEncoding?:any):string|Uint8Array|null { return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; } -diag():string|null -diag(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null -diag(optionalEncoding?:any):string|Uint8Array|null { - const offset = this.bb!.__offset(this.bb_pos, 6); - return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; -} - static startWalletRewardRequest(builder:flatbuffers.Builder) { - builder.startObject(2); + builder.startObject(1); } static addNonce(builder:flatbuffers.Builder, nonceOffset:flatbuffers.Offset) { builder.addFieldOffset(0, nonceOffset, 0); } -static addDiag(builder:flatbuffers.Builder, diagOffset:flatbuffers.Offset) { - builder.addFieldOffset(1, diagOffset, 0); -} - static endWalletRewardRequest(builder:flatbuffers.Builder):flatbuffers.Offset { const offset = builder.endObject(); return offset; } -static createWalletRewardRequest(builder:flatbuffers.Builder, nonceOffset:flatbuffers.Offset, diagOffset:flatbuffers.Offset):flatbuffers.Offset { +static createWalletRewardRequest(builder:flatbuffers.Builder, nonceOffset:flatbuffers.Offset):flatbuffers.Offset { WalletRewardRequest.startWalletRewardRequest(builder); WalletRewardRequest.addNonce(builder, nonceOffset); - WalletRewardRequest.addDiag(builder, diagOffset); return WalletRewardRequest.endWalletRewardRequest(builder); } } diff --git a/ui/src/lib/ads.ts b/ui/src/lib/ads.ts index 573d76c..f68a442 100644 --- a/ui/src/lib/ads.ts +++ b/ui/src/lib/ads.ts @@ -17,12 +17,10 @@ export function adsStubEnabled(): boolean { return (import.meta.env as Record).VITE_ADS_STUB === '1'; } -/** RewardedResult is one rewarded-ad view: whether it was watched, the raw provider result as JSON - * (forwarded for the contour diagnostic), and whether it was the test stub (so the caller can show - * the "ad fired" marker toast). */ +/** RewardedResult is one rewarded-ad view: whether it was watched, and whether it was the test stub + * (so the caller can show the "ad fired" marker toast). */ export interface RewardedResult { watched: boolean; - data: string; stub: boolean; } @@ -44,8 +42,7 @@ export async function rewardedReady(): Promise { */ export async function showRewarded(): Promise { if (adsStubEnabled()) { - return { watched: true, data: JSON.stringify({ stub: true }), stub: true }; + return { watched: true, stub: true }; } - const r = await vkShowRewarded(); - return { watched: r.watched, data: r.data, stub: false }; + return { watched: await vkShowRewarded(), stub: false }; } diff --git a/ui/src/lib/client.ts b/ui/src/lib/client.ts index c999a03..a79ba4c 100644 --- a/ui/src/lib/client.ts +++ b/ui/src/lib/client.ts @@ -152,9 +152,9 @@ export interface GatewayClient { * client opens; chips are credited later, by the verified server callback. Direct rail only. */ walletOrder(productId: string): Promise; /** walletReward credits a watched rewarded video (VK ads) and returns the updated wallet. nonce is - * the per-view idempotency key; diag carries the raw VK ad result for the contour diagnostic. - * Client-attested + a server daily/hourly cap; a reached cap rejects with a domain code. */ - walletReward(nonce: string, diag: string): Promise; + * the per-view idempotency key. Client-attested + a server daily/hourly cap; a reached cap rejects + * with a domain code. */ + walletReward(nonce: string): Promise; // --- friends --- friendsList(): Promise; diff --git a/ui/src/lib/codec.test.ts b/ui/src/lib/codec.test.ts index 9dcc6e6..3bc24df 100644 --- a/ui/src/lib/codec.test.ts +++ b/ui/src/lib/codec.test.ts @@ -78,12 +78,11 @@ describe('codec', () => { expect(w.rewardChips).toBe(7); }); - it('round-trips the wallet reward request (nonce + diag)', () => { + it('round-trips the wallet reward request (nonce)', () => { const req = fb.WalletRewardRequest.getRootAsWalletRewardRequest( - new ByteBuffer(encodeWalletReward('nonce-9', '{"result":true}')), + new ByteBuffer(encodeWalletReward('nonce-9')), ); expect(req.nonce()).toBe('nonce-9'); - expect(req.diag()).toBe('{"result":true}'); }); it('round-trips the wallet order request and response', () => { diff --git a/ui/src/lib/codec.ts b/ui/src/lib/codec.ts index 801e493..f229873 100644 --- a/ui/src/lib/codec.ts +++ b/ui/src/lib/codec.ts @@ -403,14 +403,12 @@ export function decodeWallet(buf: Uint8Array): Wallet { } // encodeWalletReward wraps a watched rewarded video for crediting (POST /user/wallet/reward): the -// per-view idempotency nonce and the raw VK ad result forwarded for the contour diagnostic. -export function encodeWalletReward(nonce: string, diag: string): Uint8Array { +// per-view idempotency nonce. +export function encodeWalletReward(nonce: string): Uint8Array { const b = new Builder(64); const n = b.createString(nonce); - const d = b.createString(diag); fb.WalletRewardRequest.startWalletRewardRequest(b); fb.WalletRewardRequest.addNonce(b, n); - fb.WalletRewardRequest.addDiag(b, d); return finish(b, fb.WalletRewardRequest.endWalletRewardRequest(b)); } diff --git a/ui/src/lib/mock/client.ts b/ui/src/lib/mock/client.ts index 5622a7d..0e6363c 100644 --- a/ui/src/lib/mock/client.ts +++ b/ui/src/lib/mock/client.ts @@ -236,7 +236,7 @@ export class MockGateway implements GatewayClient { return { orderId: 'mock-order-' + productId, redirectUrl: 'https://mock.local/robokassa?order=' + productId }; } - async walletReward(_nonce: string, _diag: string): Promise { + async walletReward(_nonce: string): Promise { // Mock rewarded credit: add the configured payout to the vk segment (no cap in the mock). const vk = this.mockWallet.segments.find((s) => s.source === 'vk'); if (vk) vk.chips += this.mockWallet.rewardChips; diff --git a/ui/src/lib/transport.ts b/ui/src/lib/transport.ts index 9fe973b..2c066da 100644 --- a/ui/src/lib/transport.ts +++ b/ui/src/lib/transport.ts @@ -240,8 +240,8 @@ export function createTransport(baseUrl: string): GatewayClient { async walletOrder(productId: string) { return codec.decodeWalletOrder(await exec('wallet.order', codec.encodeWalletOrder(productId))); }, - async walletReward(nonce: string, diag: string) { - return codec.decodeWallet(await exec('wallet.reward', codec.encodeWalletReward(nonce, diag))); + async walletReward(nonce: string) { + return codec.decodeWallet(await exec('wallet.reward', codec.encodeWalletReward(nonce))); }, async friendsList() { diff --git a/ui/src/lib/vk.ts b/ui/src/lib/vk.ts index 6a0ab2f..bca6746 100644 --- a/ui/src/lib/vk.ts +++ b/ui/src/lib/vk.ts @@ -213,16 +213,15 @@ export async function vkRewardedReady(): Promise { /** * vkShowRewarded shows a rewarded ad (VKWebAppShowNativeAds) and reports whether it was watched - * (data.result) plus the full raw provider result as JSON — forwarded to the backend for the - * temporary contour diagnostic (to confirm exactly what VK returns). A rejected call reports - * not-watched with the error captured. + * (data.result — VK returns only this boolean, no server-verifiable token). A rejected call reports + * not-watched. */ -export async function vkShowRewarded(): Promise<{ watched: boolean; data: string }> { +export async function vkShowRewarded(): Promise { try { const data = await (await bridge()).send('VKWebAppShowNativeAds', { ad_format: 'reward' }); - return { watched: !!(data as { result?: boolean }).result, data: JSON.stringify(data) }; - } catch (e) { - return { watched: false, data: JSON.stringify({ error: String(e) }) }; + return !!(data as { result?: boolean }).result; + } catch { + return false; } } diff --git a/ui/src/screens/Wallet.svelte b/ui/src/screens/Wallet.svelte index 57842bd..332b00a 100644 --- a/ui/src/screens/Wallet.svelte +++ b/ui/src/screens/Wallet.svelte @@ -63,7 +63,7 @@ } const nonce = crypto.randomUUID(); const before = wallet?.segments.find((s) => s.source === 'vk')?.chips ?? 0; - const w = await gateway.walletReward(nonce, res.data); + const w = await gateway.walletReward(nonce); wallet = w; const gained = (w.segments.find((s) => s.source === 'vk')?.chips ?? 0) - before; showToast(gained > 0 ? t('wallet.rewardCredited', { n: gained }) : t('wallet.rewardCredited', { n: w.rewardChips }));