@quenty/gameproductservice 14.51.1 → 14.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [14.52.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/gameproductservice@14.51.2...@quenty/gameproductservice@14.52.0) (2026-07-02)
7
+
8
+ ### Bug Fixes
9
+
10
+ - Fix lint ([c9c1ed7](https://github.com/Quenty/NevermoreEngine/commit/c9c1ed7ee47bd464225c64baffbabbdeee6ddafe))
11
+ - Fix property replication ([cbdf092](https://github.com/Quenty/NevermoreEngine/commit/cbdf092195a1f11f402aa22f53ce29d4f93eb00d))
12
+
13
+ ### Features
14
+
15
+ - Add server-only purchasing mode to GameProductService ([af6551b](https://github.com/Quenty/NevermoreEngine/commit/af6551b2f8d3bf202cb8dfa98f10fd1885b31668))
16
+
17
+ ## [14.51.2](https://github.com/Quenty/NevermoreEngine/compare/@quenty/gameproductservice@14.51.1...@quenty/gameproductservice@14.51.2) (2026-06-03)
18
+
19
+ **Note:** Version bump only for package @quenty/gameproductservice
20
+
6
21
  ## [14.51.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/gameproductservice@14.51.0...@quenty/gameproductservice@14.51.1) (2026-05-30)
7
22
 
8
23
  **Note:** Version bump only for package @quenty/gameproductservice
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/gameproductservice",
3
- "version": "14.51.1",
3
+ "version": "14.52.0",
4
4
  "description": "Generalized monetization system for handling products and purchases correctly.",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -35,9 +35,9 @@
35
35
  "@quenty/baseobject": "10.13.0",
36
36
  "@quenty/binder": "14.36.1",
37
37
  "@quenty/brio": "14.30.1",
38
- "@quenty/cmdrservice": "13.45.1",
38
+ "@quenty/cmdrservice": "13.45.2",
39
39
  "@quenty/enumutils": "3.4.6",
40
- "@quenty/gameconfig": "12.49.1",
40
+ "@quenty/gameconfig": "12.49.2",
41
41
  "@quenty/instanceutils": "13.30.1",
42
42
  "@quenty/loader": "10.11.0",
43
43
  "@quenty/maid": "3.9.0",
@@ -60,5 +60,5 @@
60
60
  "@quenty/tie": "10.40.1",
61
61
  "@quenty/valueobject": "13.31.1"
62
62
  },
63
- "gitHead": "598b2b62b36bdcbdbbd56f7db10c399831cc6eba"
63
+ "gitHead": "76cd9189962481d0932514d8348fb0ed2c0454d8"
64
64
  }
@@ -146,6 +146,17 @@ function GameProductServiceClient:PromisePromptPurchase(
146
146
  return self._gameProductDataService:PromisePromptPurchase(player, assetType, idOrKey)
147
147
  end
148
148
 
149
+ --[=[
150
+ Observes whether server-only prompting is enabled. When enabled,
151
+ [GameProductServiceClient:PromisePromptPurchase] rejects and prompts must be
152
+ initiated from the server. Useful for hiding or disabling buy buttons on the client.
153
+
154
+ @return Observable<boolean>
155
+ ]=]
156
+ function GameProductServiceClient:ObserveServerOnlyPromptingEnabled(): Observable.Observable<boolean>
157
+ return self._gameProductDataService:ObserveServerOnlyPrompting(Players.LocalPlayer)
158
+ end
159
+
149
160
  --[=[
150
161
  Returns true if item has been purchased this session
151
162
 
@@ -249,6 +249,29 @@ function GameProductService.ObservePlayerOwnership(
249
249
  return self._gameProductDataService:ObservePlayerOwnership(player, assetType, idOrKey)
250
250
  end
251
251
 
252
+ --[=[
253
+ Restricts purchase prompting to the server. When enabled, clients can no longer
254
+ prompt purchases directly via [GameProductServiceClient:PromisePromptPurchase] (it
255
+ rejects); prompts must be initiated from the server, where the developer can validate
256
+ the asset id. Defaults to disabled.
257
+
258
+ @param enabled boolean
259
+ ]=]
260
+ function GameProductService.SetServerOnlyPromptingEnabled(self: GameProductService, enabled: boolean)
261
+ assert(type(enabled) == "boolean", "Bad enabled")
262
+
263
+ self._gameProductDataService:SetServerOnlyPrompting(enabled)
264
+ end
265
+
266
+ --[=[
267
+ Returns true if server-only prompting is enabled.
268
+
269
+ @return boolean
270
+ ]=]
271
+ function GameProductService.IsServerOnlyPromptingEnabled(self: GameProductService): boolean
272
+ return self._gameProductDataService:GetServerOnlyPromptingValue().Value
273
+ end
274
+
252
275
  --[=[
253
276
  Cleans up the game product service
254
277
  ]=]
@@ -13,6 +13,7 @@ local Players = game:GetService("Players")
13
13
 
14
14
  local EnumUtils = require("EnumUtils")
15
15
  local GameConfigAssetTypes = require("GameConfigAssetTypes")
16
+ local GameProductDataService = require("GameProductDataService")
16
17
  local PlayerBinder = require("PlayerBinder")
17
18
  local PlayerProductManagerBase = require("PlayerProductManagerBase")
18
19
  local PlayerProductManagerInterface = require("PlayerProductManagerInterface")
@@ -60,6 +61,11 @@ function PlayerProductManager.new(player: Player, serviceBag: ServiceBag.Service
60
61
 
61
62
  -- Initialize attributes
62
63
 
64
+ local serverOnlyPrompting = self._serviceBag:GetService(GameProductDataService):GetServerOnlyPromptingValue()
65
+ self._maid:GiveTask(serverOnlyPrompting:Observe():Subscribe(function(value)
66
+ self._obj:SetAttribute(GameProductDataService.ServerOnlyPromptingAttribute, value)
67
+ end))
68
+
63
69
  -- Implement
64
70
  local impl = self._maid:Add(PlayerProductManagerInterface.Server:Implement(self._obj, self))
65
71
  self:ExportMarketTrackers(impl:GetImplParent())
@@ -13,19 +13,25 @@ local Observable = require("Observable")
13
13
  local PlayerProductManagerInterface = require("PlayerProductManagerInterface")
14
14
  local Promise = require("Promise")
15
15
  local Rx = require("Rx")
16
+ local RxAttributeUtils = require("RxAttributeUtils")
16
17
  local RxBrioUtils = require("RxBrioUtils")
17
18
  local ServiceBag = require("ServiceBag")
18
19
  local Signal = require("Signal")
19
20
  local TieRealmService = require("TieRealmService")
21
+ local TieRealms = require("TieRealms")
22
+ local ValueObject = require("ValueObject")
20
23
 
21
24
  local GameProductDataService = {}
22
25
  GameProductDataService.ServiceName = "GameProductDataService"
23
26
 
27
+ GameProductDataService.ServerOnlyPromptingAttribute = "GameProductServerOnlyPrompting"
28
+
24
29
  export type GameProductDataService = typeof(setmetatable(
25
30
  {} :: {
26
31
  _serviceBag: ServiceBag.ServiceBag,
27
32
  _tieRealmService: TieRealmService.TieRealmService,
28
33
  _maid: Maid.Maid,
34
+ _serverOnlyPrompting: ValueObject.ValueObject<boolean>,
29
35
 
30
36
  GamePassPurchased: Signal.Signal<Player, number>,
31
37
  ProductPurchased: Signal.Signal<Player, number>,
@@ -46,6 +52,12 @@ function GameProductDataService.Init(self: GameProductDataService, serviceBag: S
46
52
 
47
53
  self._maid = Maid.new()
48
54
 
55
+ -- Source of truth for server-only prompting. On the server this is set via
56
+ -- [GameProductService:SetServerOnlyPromptingEnabled] and replicated to clients
57
+ -- through the PlayerProductManager tie. On the client we read the replicated
58
+ -- value to decide whether to refuse local prompts.
59
+ self._serverOnlyPrompting = self._maid:Add(ValueObject.new(false, "boolean"))
60
+
49
61
  -- Configure
50
62
  self.GamePassPurchased = self._maid:Add(Signal.new() :: any) -- :Fire(player, gamePassId)
51
63
  self.ProductPurchased = self._maid:Add(Signal.new() :: any) -- :Fire(player, productId)
@@ -84,6 +96,111 @@ function GameProductDataService.Start(self: GameProductDataService)
84
96
  )
85
97
  end
86
98
 
99
+ --[=[
100
+ Sets whether prompting is restricted to the server. When enabled, clients can no
101
+ longer prompt purchases directly via [GameProductDataService:PromisePromptPurchase];
102
+ prompts must be initiated from the server. Can only be set from the server.
103
+
104
+ @param serverOnly boolean
105
+ ]=]
106
+ function GameProductDataService.SetServerOnlyPrompting(self: GameProductDataService, serverOnly: boolean): ()
107
+ assert(type(serverOnly) == "boolean", "Bad serverOnly")
108
+ assert(
109
+ self._tieRealmService:GetTieRealm() ~= TieRealms.CLIENT,
110
+ "[GameProductDataService] - Server-only prompting can only be configured from the server"
111
+ )
112
+
113
+ self._serverOnlyPrompting.Value = serverOnly
114
+ end
115
+
116
+ --[=[
117
+ Returns the server-authoritative server-only prompting [ValueObject]. This is
118
+ exported through the PlayerProductManager tie so it replicates to clients.
119
+
120
+ @return ValueObject<boolean>
121
+ ]=]
122
+ function GameProductDataService.GetServerOnlyPromptingValue(
123
+ self: GameProductDataService
124
+ ): ValueObject.ValueObject<boolean>
125
+ return self._serverOnlyPrompting
126
+ end
127
+
128
+ --[=[
129
+ Returns true if server-only prompting is currently enabled. On the client this
130
+ reads the value replicated from the server for the local player.
131
+
132
+ @param player Player
133
+ @return Observable<boolean>
134
+ ]=]
135
+ function GameProductDataService.ObserveServerOnlyPrompting(
136
+ _self: GameProductDataService,
137
+ player: Player
138
+ ): Observable.Observable<boolean>
139
+ assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
140
+
141
+ -- Always read the server-authoritative value (the server implementation), which
142
+ -- replicates down to clients as an attribute on the PlayerProductManager folder.
143
+ return RxAttributeUtils.observeAttribute(
144
+ player,
145
+ GameProductDataService.ServerOnlyPromptingAttribute,
146
+ false
147
+ )
148
+ :Pipe({
149
+ Rx.map(function(value: any)
150
+ return value == true
151
+ end) :: any,
152
+ Rx.distinct() :: any,
153
+ }) :: any
154
+ end
155
+
156
+ --[=[
157
+ Resolves if the caller is allowed to prompt a purchase, rejecting if server-only
158
+ prompting is enabled and we are on the client. The server is always authorized.
159
+
160
+ @param player Player
161
+ @return Promise
162
+ ]=]
163
+ function GameProductDataService._promiseServerOnlyPromptingGuard(
164
+ self: GameProductDataService,
165
+ player: Player
166
+ ): Promise.Promise<()>
167
+ -- The server (and a single shared realm) is the authority and may always prompt.
168
+ if self._tieRealmService:GetTieRealm() ~= TieRealms.CLIENT then
169
+ return Promise.resolved()
170
+ end
171
+
172
+ return Rx.toPromise(self:ObserveServerOnlyPrompting(player) :: any):Then(function(serverOnly)
173
+ if serverOnly then
174
+ return Promise.rejected(
175
+ "[GameProductDataService] - Server-only prompting is enabled. The client cannot prompt purchases "
176
+ .. "directly; prompts must be initiated from the server."
177
+ )
178
+ end
179
+
180
+ return nil
181
+ end)
182
+ end
183
+
184
+ --[=[
185
+ Applies the server-only prompting guard and then prompts the purchase. Used by all
186
+ prompt entry points so the guard is enforced consistently.
187
+
188
+ @param player Player
189
+ @param assetTracker PlayerAssetMarketTracker
190
+ @param idOrKey string | number
191
+ @return Promise<boolean>
192
+ ]=]
193
+ function GameProductDataService._promiseGuardedPromptPurchase(
194
+ self: GameProductDataService,
195
+ player: Player,
196
+ assetTracker: any,
197
+ idOrKey: string | number
198
+ ): Promise.Promise<boolean>
199
+ return self:_promiseServerOnlyPromptingGuard(player):Then(function()
200
+ return assetTracker:PromisePromptPurchase(idOrKey)
201
+ end)
202
+ end
203
+
87
204
  --[=[
88
205
  Returns true if item has been purchased this session
89
206
 
@@ -132,7 +249,7 @@ function GameProductDataService.PromisePromptPurchase(
132
249
 
133
250
  return self:_promisePlayerProductManager(player):Then(function(playerProductManager)
134
251
  local assetTracker = playerProductManager:GetAssetTrackerOrError(assetType)
135
- return assetTracker:PromisePromptPurchase(idOrKey)
252
+ return self:_promiseGuardedPromptPurchase(player, assetTracker, idOrKey)
136
253
  end)
137
254
  end
138
255
 
@@ -350,7 +467,7 @@ function GameProductDataService.PromisePlayerOwnershipOrPrompt(
350
467
  if ownsAsset then
351
468
  return true
352
469
  else
353
- return assetTracker:PromisePromptPurchase(idOrKey)
470
+ return self:_promiseGuardedPromptPurchase(player, assetTracker, idOrKey) :: any
354
471
  end
355
472
  end)
356
473
  else
@@ -359,7 +476,7 @@ function GameProductDataService.PromisePlayerOwnershipOrPrompt(
359
476
  return Promise.resolved(true)
360
477
  end
361
478
 
362
- return assetTracker:PromisePromptPurchase(idOrKey)
479
+ return self:_promiseGuardedPromptPurchase(player, assetTracker, idOrKey)
363
480
  end
364
481
  end)
365
482
  end