fix(payments): VK-iOS freeze is purchase-only; remove the rewarded diagnostic
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m9s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m5s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m9s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m5s
Testing the rewarded slice surfaced a contradiction: rewarded ads let a VK-iOS
user earn vk chips, but the blanket VK-iOS "spend freeze" then blocked spending
them (the wallet showed "5 (view-only)"). Apple's ToS forbids only BUYING in-app
values on VK-iOS (money -> chips), not spending or earning them — so the freeze is
corrected to purchase-only: vkFrozen() now gates only CreateOrder (the money-in
step), not spendableSources (spending). VK-wallet chips — earned via rewarded ads
or bought on the same account elsewhere (VK Android) — now spend on VK-iOS too.
(Owner's ToS finding.)
Also: the temporary contour diagnostic confirmed VK returns only {result:true} for
a rewarded view (no token/signature) — client-attested is final, no hardening
possible — so the diagnostic (the log and the diag wire field) is removed.
Docs: PAYMENTS(+ru) VK-iOS freeze + D17 amend + PLAN E6. Tests updated (gate /
wallet VK-iOS now spendable).
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user