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 use | It gives you |
|---|---|
IProjectVShopScreen | The screen contract: observe the offers, buy one |
ProjectVShopView | What to draw: loading state and the list of offers |
ProjectVShopOfferView | One offer: price, quantity range, purchase limit |
ProjectVResult | The 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:
| Member | Type | Use it for |
|---|---|---|
IsLoading | bool | True until the offers arrive the first time. Show a spinner |
LoadError | string? | Set when that first load failed. Show it with a Retry button that closes and reopens the page |
Offers | IReadOnlyList<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 show | Read |
|---|---|
| The price | WalletPrice in WalletCurrencyId (for example 50 core_dust) |
| The quantity picker | From QuantityMin to QuantityMax |
| "3/5 bought today" | PurchaseCount / LimitCount, with LimitKind (Daily, Weekly, Monthly, Forever) |
| Sold out | PurchaseLimitReached |
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)
| Parameter | Type | Value |
|---|---|---|
offerId | string | offer.OfferId.Value of an offer in the current view |
quantity | int | From QuantityMin to QuantityMax |
Returns a ProjectVResult:
IsSuccess | Message | What happened |
|---|---|---|
true | empty | The server confirmed the purchase. The page and the Header redraw by themselves |
false | the reason, ready to show | The purchase did not happen, or could not be confirmed (see below) |
When it fails โ show Message as it is. The common reasons:
| Situation | What the player sees |
|---|---|
| Not enough currency | The server's message (code SHOP_INSUFFICIENT_FUNDS) |
| Limit already reached | The server's message (code SHOP_LIMIT_REACHED) |
| The offer changed or was removed on the server | The 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 timeout | A 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, whenLimitKindis notNone), and what the player can afford (balance / WalletPrice, using the Header balance ofWalletCurrencyId). - Disable Buy when that range is empty.
- Hide a
Foreveroffer oncePurchaseLimitReachedis 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.
Observealready 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 != nullafterawait. - Not disposing the observation. Keep the
IDisposablefromObserveand dispose it inOnDisableorOnDestroy.
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.