@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.
|
|
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.
|
|
38
|
+
"@quenty/cmdrservice": "13.45.2",
|
|
39
39
|
"@quenty/enumutils": "3.4.6",
|
|
40
|
-
"@quenty/gameconfig": "12.49.
|
|
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": "
|
|
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
|
|
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
|
|
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
|
|
479
|
+
return self:_promiseGuardedPromptPurchase(player, assetTracker, idOrKey)
|
|
363
480
|
end
|
|
364
481
|
end)
|
|
365
482
|
end
|