Skip to main content

Shop

The Shop screen lists what the server sells and lets the player buy it with in-game currency (Core Dust and so on). Prices, purchase limits and what a purchase gives are decided by the server.

You useIt gives you
IProjectVShopScreenThe screen contract: observe the offers, buy one
ProjectVShopViewWhat to draw: loading state and the list of offers
ProjectVShopOfferViewOne offer: price, quantity range, purchase limit
ProjectVResultThe outcome of a purchase

New to the Shop? Follow the tutorial first.

Quick startโ€‹

using System;
using System.Threading.Tasks;
using SilverTiger.ProjectV.Unity.Presentation.Shop;
using UnityEngine;
using VContainer;

public sealed class ShopPage : MonoBehaviour
{
private IProjectVShopScreen shop = null!;
private IDisposable? observation;

[Inject]
public void Construct(IProjectVShopScreen screen) => shop = screen;

private void OnEnable() => observation = shop.Observe(Render);

private void OnDisable()
{
observation?.Dispose();
observation = null;
}

private void Render(ProjectVShopView view)
{
if (view.IsLoading)
{
ShowLoading(view.LoadError); // LoadError is null while the first load is running
return;
}
ShowOffers(view.Offers);
}

public async void OnBuyClicked(string offerId, int quantity)
{
var result = await shop.PurchaseAsync(offerId, quantity);
if (this != null && !result.IsSuccess)
{
ShowMessage(result.Message);
}
}

private void ShowLoading(string? error) { }
private void ShowOffers(System.Collections.Generic.IReadOnlyList<ProjectVShopOfferView> offers) { }
private void ShowMessage(string message) { }
}

That is the whole page: observe, draw, buy. After a successful purchase you do nothing; Render is called again with the new purchase counts, and the Header shows the new balance.

What to drawโ€‹

Render receives a ProjectVShopView:

MemberTypeUse it for
IsLoadingboolTrue until the offers arrive the first time. Show a spinner
LoadErrorstring?Set when that first load failed. Show it with a Retry button that closes and reopens the page
OffersIReadOnlyList<ProjectVShopOfferView>One entry per offer, in server order
Find(offerId)ProjectVShopOfferView?Look up one offer, for example to refresh an open purchase popup

For each ProjectVShopOfferView:

To showRead
The priceWalletPrice in WalletCurrencyId (for example 50 core_dust)
The quantity pickerFrom QuantityMin to QuantityMax
"3/5 bought today"PurchaseCount / LimitCount, with LimitKind (Daily, Weekly, Monthly, Forever)
Sold outPurchaseLimitReached

Match an offer to your art and text with OfferId.Value in a table of your own; the SDK does not provide names or icons.

Buy an offerโ€‹

Task<ProjectVResult> PurchaseAsync(string offerId, int quantity)
ParameterTypeValue
offerIdstringoffer.OfferId.Value of an offer in the current view
quantityintFrom QuantityMin to QuantityMax

Returns a ProjectVResult:

IsSuccessMessageWhat happened
trueemptyThe server confirmed the purchase. The page and the Header redraw by themselves
falsethe reason, ready to showThe purchase did not happen, or could not be confirmed (see below)

When it fails โ€” show Message as it is. The common reasons:

SituationWhat the player sees
Not enough currencyThe server's message (code SHOP_INSUFFICIENT_FUNDS)
Limit already reachedThe server's message (code SHOP_LIMIT_REACHED)
The offer changed or was removed on the serverThe server's message (codes SHOP_STALE_OFFER, SHOP_OFFER_NOT_FOUND); the list redraws with the current offers
The offerId is not in the current view"This offer is no longer available."
A store (IsIap) offer"This offer must be purchased through the platform store."
No connection or a timeoutA network message. The purchase may still have gone through, so do not buy again automatically

A second tap while a purchase is running gets the same result instead of buying twice. After a failure, let the player decide whether to try again.

Quantity pickerโ€‹

  • Start at QuantityMin.
  • The maximum is the smallest of: QuantityMax, what is left of the limit (LimitCount - PurchaseCount, when LimitKind is not None), and what the player can afford (balance / WalletPrice, using the Header balance of WalletCurrencyId).
  • Disable Buy when that range is empty.
  • Hide a Forever offer once PurchaseLimitReached is true; keep offers that reset (Daily, Weekly, Monthly) visible as sold out.

These rules only shape the picker. The server checks every purchase anyway.

Common mistakesโ€‹

  • Adding the purchase to the balance yourself. The Header updates by itself; your number would be wrong.
  • Calling a refresh after buying, or when the page opens. Observe already does everything needed.
  • Disabling the Buy button to stop double taps. Not needed; a second tap joins the first purchase.
  • Using the result after the page closed. Check this != null after await.
  • Not disposing the observation. Keep the IDisposable from Observe and dispose it in OnDisable or OnDestroy.

Presenterโ€‹

IProjectVShopPresenter is the lower-level interface behind the screen contract. It also exposes the last purchase Receipt and a full ProjectVShopError with Code and RequestId. Use it only if you need those; pages normally use IProjectVShopScreen.