@quenty/gameproductservice 5.15.0 → 6.0.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,14 @@
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
+ # [6.0.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/gameproductservice@5.15.0...@quenty/gameproductservice@6.0.0) (2023-02-27)
7
+
8
+ **Note:** Version bump only for package @quenty/gameproductservice
9
+
10
+
11
+
12
+
13
+
6
14
  # [5.15.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/gameproductservice@5.14.0...@quenty/gameproductservice@5.15.0) (2023-02-27)
7
15
 
8
16
  **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": "5.15.0",
3
+ "version": "6.0.0",
4
4
  "description": "Generalized monetization system for handling products and purchases correctly.",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -34,6 +34,7 @@
34
34
  "@quenty/loader": "^6.1.0",
35
35
  "@quenty/maid": "^2.4.0",
36
36
  "@quenty/marketplaceutils": "^6.2.0",
37
+ "@quenty/observablecollection": "^5.8.0",
37
38
  "@quenty/playerbinder": "^8.8.0",
38
39
  "@quenty/promise": "^6.2.0",
39
40
  "@quenty/remoting": "^6.2.0",
@@ -42,7 +43,8 @@
42
43
  "@quenty/servicebag": "^6.5.0",
43
44
  "@quenty/signal": "^2.3.0",
44
45
  "@quenty/statestack": "^8.6.0",
46
+ "@quenty/string": "^3.1.0",
45
47
  "@quenty/table": "^3.2.0"
46
48
  },
47
- "gitHead": "62c1fdb2f329ca6d6395b63a56a5dfacb3831349"
49
+ "gitHead": "ef2767286911d37d3584dd21efee66b88061cb6e"
48
50
  }
@@ -1,20 +1,35 @@
1
1
  --[=[
2
+ This service provides an interface to purchase produces, assets, and other
3
+ marketplace items. This listens to events, handles requests between server and
4
+ client, and takes in both assetKeys from GameConfigService, as well as
5
+ assetIds.
6
+
7
+ See [GameProductService] for the server equivalent. The API surface should be
8
+ effectively the same between the two.
9
+
10
+ @client
2
11
  @class GameProductServiceClient
3
12
  ]=]
4
13
 
5
14
  local require = require(script.Parent.loader).load(script)
6
15
 
7
16
  local Players = game:GetService("Players")
8
- local MarketplaceService = game:GetService("MarketplaceService")
9
17
 
10
- local Maid = require("Maid")
11
- local GameProductServiceBase = require("GameProductServiceBase")
12
- local Signal = require("Signal")
13
18
  local GameConfigAssetTypes = require("GameConfigAssetTypes")
19
+ local Maid = require("Maid")
14
20
  local Promise = require("Promise")
21
+ local RxBinderUtils = require("RxBinderUtils")
22
+ local Signal = require("Signal")
23
+ local GameProductServiceHelper = require("GameProductServiceHelper")
24
+ local GameConfigAssetTypeUtils = require("GameConfigAssetTypeUtils")
15
25
 
16
- local GameProductServiceClient = GameProductServiceBase.new()
26
+ local GameProductServiceClient = {}
27
+ GameProductServiceClient.ServiceName = "GameProductServiceClient"
17
28
 
29
+ --[=[
30
+ Initializes the service. Should be done via [ServiceBag]
31
+ @param serviceBag ServiceBag
32
+ ]=]
18
33
  function GameProductServiceClient:Init(serviceBag)
19
34
  assert(not self._serviceBag, "Already initialized")
20
35
  self._serviceBag = assert(serviceBag, "No serviceBag")
@@ -26,151 +41,158 @@ function GameProductServiceClient:Init(serviceBag)
26
41
  -- Internal
27
42
  self._binders = self._serviceBag:GetService(require("GameProductBindersClient"))
28
43
 
29
- self.GamepassPurchased = Signal.new() -- :Fire(gamepassId)
30
- self._maid:GiveTask(self.GamepassPurchased)
31
-
32
- self.DevProductPurchased = Signal.new() -- :Fire(productId)
33
- self._maid:GiveTask(self.DevProductPurchased)
34
-
35
- self._promptClosedEvent = Signal.new()
36
- self._maid:GiveTask(self._promptClosedEvent)
37
-
38
- self._purchasedGamePassesThisSession = {}
39
- self._maid:GiveTask(MarketplaceService.PromptGamePassPurchaseFinished
40
- :Connect(function(player, gamepassId, wasPurchased)
41
- if player == Players.LocalPlayer then
42
- self._promptClosedEvent:Fire()
43
- if wasPurchased then
44
- self._purchasedGamePassesThisSession[gamepassId] = true
45
- -- self._fireworksService:Create(3)
46
- self.GamepassPurchased:Fire(gamepassId)
47
- end
48
- end
49
- end))
50
-
51
- self._purchasedDevProductsThisSession = {}
52
- self._maid:GiveTask(MarketplaceService.PromptProductPurchaseFinished
53
- :Connect(function(userId, productId, wasPurchased)
54
- if userId == Players.LocalPlayer.UserId then
55
- self._promptClosedEvent:Fire()
56
- if wasPurchased then
57
- -- self._fireworksService:Create(3)
58
- self._purchasedDevProductsThisSession[productId] = true
59
- self.DevProductPurchased:Fire(productId)
60
- end
61
- end
62
- end))
44
+ self._helper = GameProductServiceHelper.new(self._binders.PlayerProductManager)
45
+ self._maid:GiveTask(self._helper)
46
+
47
+ -- Additional API for ergonomics
48
+ self.GamePassPurchased = Signal.new() -- :Fire(gamePassId)
49
+ self._maid:GiveTask(self.GamePassPurchased)
50
+
51
+ self.ProductPurchased = Signal.new() -- :Fire(productId)
52
+ self._maid:GiveTask(self.ProductPurchased)
53
+
54
+ self.AssetPurchased = Signal.new() -- :Fire(assetId)
55
+ self._maid:GiveTask(self.AssetPurchased)
56
+
57
+ self.BundlePurchased = Signal.new() -- :Fire(bundleId)
58
+ self._maid:GiveTask(self.BundlePurchased)
63
59
  end
64
60
 
65
61
  --[=[
66
- Promises whether the local player owns the pass or not
67
- @param passIdOrKey string | number
68
- @return Promise<boolean>
62
+ Starts the service. Should be done via [ServiceBag]
69
63
  ]=]
70
- function GameProductServiceClient:PromiseLocalPlayerOwnsPass(passIdOrKey)
71
- assert(type(passIdOrKey) == "number" or type(passIdOrKey) == "string", "Bad passIdOrKey")
64
+ function GameProductServiceClient:Start()
65
+ self._maid:GiveTask(RxBinderUtils.observeBoundClassBrio(self._binders.PlayerProductManager, Players.LocalPlayer):Subscribe(function(brio)
66
+ if brio:IsDead() then
67
+ return
68
+ end
69
+
70
+ local maid = brio:ToMaid()
71
+ local playerProductManager = brio:GetValue()
72
+ local playerMrketeer = playerProductManager:GetMarketeer()
73
+
74
+ local function exposeSignal(signal, assetType)
75
+ maid:GiveTask(playerMrketeer:GetAssetTrackerOrError(assetType).Purchased:Connect(function(...)
76
+ signal:Fire(...)
77
+ end))
78
+ end
79
+
80
+ exposeSignal(self.GamePassPurchased, GameConfigAssetTypes.PASS)
81
+ exposeSignal(self.ProductPurchased, GameConfigAssetTypes.PRODUCT)
82
+ exposeSignal(self.AssetPurchased, GameConfigAssetTypes.ASSET)
83
+ exposeSignal(self.BundlePurchased, GameConfigAssetTypes.BUNDLE)
84
+ end))
85
+ end
72
86
 
73
- local passId = self:ToAssetId(GameConfigAssetTypes.PASS, passIdOrKey)
74
- if not passId then
75
- return Promise.rejected(("No asset with key %q"):format(tostring(passIdOrKey)))
76
- end
87
+ --[=[
88
+ Returns true if item has been purchased this session
77
89
 
78
- if self._purchasedGamePassesThisSession[passId] == true then
79
- return Promise.resolved(self._purchasedGamePassesThisSession[passId])
80
- end
90
+ @param player Player
91
+ @param assetType GameConfigAssetType
92
+ @param idOrKey string | number
93
+ @return boolean
94
+ ]=]
95
+ function GameProductServiceClient:HasPlayerPurchasedThisSession(player, assetType, idOrKey)
96
+ assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
97
+ assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
98
+ assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
81
99
 
82
- return self:PromisePlayerOwnsPass(Players.LocalPlayer, passId)
100
+ return self._helper:HasPlayerPurchasedThisSession(player, assetType, idOrKey)
83
101
  end
84
102
 
85
103
  --[=[
86
- Observes whether the local player owns the pass or not
87
- @param passIdOrKey string | number
88
- @return Observable<boolean>
104
+ Prompts the user to purchase the asset, and returns true if purchased
105
+
106
+ @param player Player
107
+ @param assetType GameConfigAssetType
108
+ @param idOrKey string | number
109
+ @return Promise<boolean>
89
110
  ]=]
90
- function GameProductServiceBase:ObserveLocalPlayerOwnsPass(passIdOrKey)
91
- assert(type(passIdOrKey) == "number" or type(passIdOrKey) == "string", "Bad passIdOrKey")
111
+ function GameProductServiceClient:PromisePromptPurchase(player, assetType, idOrKey)
112
+ assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
113
+ assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
114
+ assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
92
115
 
93
- return self:ObservePlayerOwnsPass(Players.LocalPlayer, passIdOrKey)
94
- end
95
116
 
96
- function GameProductServiceClient:GetPlayerProductManagerBinder()
97
- return self._binders.PlayerProductManager
117
+ return self._helper:PromisePromptPurchase(player, assetType, idOrKey)
98
118
  end
99
119
 
100
- function GameProductServiceClient:FlagPromptOpen()
101
- assert(self ~= GameProductServiceClient, "Use serviceBag")
102
- assert(self._serviceBag, "Not initialized")
120
+ --[=[
121
+ Returns true if item has been purchased this session
103
122
 
104
- self._promptOpenFlag = true
123
+ @param player Player
124
+ @param assetType GameConfigAssetType
125
+ @param idOrKey string | number
126
+ @return Promise<boolean>
127
+ ]=]
128
+ function GameProductServiceClient:PromisePlayerOwnership(player, assetType, idOrKey)
129
+ assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
130
+ assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
131
+ assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
132
+
133
+ return self._helper:PromisePromptPurchase(player, assetType, idOrKey)
105
134
  end
106
135
 
107
- function GameProductServiceClient:GuessIfPromptOpenFromFlags()
108
- assert(self ~= GameProductServiceClient, "Use serviceBag")
109
- assert(self._serviceBag, "Not initialized")
136
+ --[=[
137
+ Observes if the player owns this cloud asset or not
110
138
 
111
- return self._promptOpenFlag
139
+ @param player Player
140
+ @param assetType GameConfigAssetType
141
+ @param idOrKey string | number
142
+ @return Observable<boolean>
143
+ ]=]
144
+ function GameProductServiceClient:ObservePlayerOwnership(player, assetType, idOrKey)
145
+ assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
146
+ assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
147
+ assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
148
+
149
+ return self._helper:ObservePlayerOwnership(player, assetType, idOrKey)
112
150
  end
113
151
 
114
- function GameProductServiceClient:HasPurchasedProductThisSession(productIdOrKey)
152
+ --[=[
153
+ Flags the propmt is open
154
+ ]=]
155
+ function GameProductServiceClient:FlagPromptOpen()
115
156
  assert(self ~= GameProductServiceClient, "Use serviceBag")
116
157
  assert(self._serviceBag, "Not initialized")
117
- assert(type(productIdOrKey) == "number" or type(productIdOrKey) == "string", "productIdOrKey")
118
-
119
- local productId = self:ToAssetId(GameConfigAssetTypes.PRODUCT, productIdOrKey)
120
- if not productId then
121
- warn(("No asset with key %q"):format(tostring(productIdOrKey)))
122
- return false
123
- end
124
-
125
- if self._purchasedDevProductsThisSession[productIdOrKey] then
126
- return true
127
- end
128
158
 
129
- return false
159
+ self._promptOpenFlag = true
130
160
  end
131
161
 
132
- function GameProductServiceClient:PromisePurchasedOrPrompt(passIdOrKey)
133
- local gamepassId = self:ToAssetId(GameConfigAssetTypes.PASS, passIdOrKey)
134
- if not gamepassId then
135
- return Promise.rejected(("No asset with key %q"):format(tostring(passIdOrKey)))
136
- end
137
162
 
138
- return self:PromiseLocalPlayerOwnsPass(gamepassId)
139
- :Then(function(owned)
140
- if not owned then
141
- MarketplaceService:PromptGamePassPurchase(Players.LocalPlayer, gamepassId)
142
- end
163
+ --[=[
164
+ Returns true if the prompt is open
165
+ @return boolean
166
+ ]=]
167
+ function GameProductServiceClient:GuessIfPromptOpenFromFlags()
168
+ assert(self ~= GameProductServiceClient, "Use serviceBag")
169
+ assert(self._serviceBag, "Not initialized")
143
170
 
144
- return owned
145
- end)
171
+ return self._promptOpenFlag
146
172
  end
147
173
 
148
- function GameProductServiceClient:PromiseGamepassOrProductUnlockOrPrompt(passIdOrKey, productIdOrKey)
149
- assert(passIdOrKey, "Bad passIdOrKey")
150
- assert(productIdOrKey, "Bad productIdOrKey")
151
-
152
- local productId = self:ToAssetId(GameConfigAssetTypes.PRODUCT, productIdOrKey)
153
- if not productId then
154
- return Promise.rejected(("No asset with key %q"):format(tostring(productIdOrKey)))
155
- end
174
+ --[=[
175
+ Promises to either check a gamepass or a product to see if it's purchased.
156
176
 
157
- local gamepassId = self:ToAssetId(GameConfigAssetTypes.PASS, passIdOrKey)
158
- if not gamepassId then
159
- return Promise.rejected(("No asset with key %q"):format(tostring(passIdOrKey)))
160
- end
177
+ @param gamePassIdOrKey string | number
178
+ @param productIdOrKey string | number
179
+ @return boolean
180
+ ]=]
181
+ function GameProductServiceClient:PromiseGamePassOrProductUnlockOrPrompt(gamePassIdOrKey, productIdOrKey)
182
+ assert(type(gamePassIdOrKey) == "number" or type(gamePassIdOrKey) == "string", "Bad gamePassIdOrKey")
183
+ assert(type(productIdOrKey) == "number" or type(productIdOrKey) == "string", "Bad productIdOrKey")
161
184
 
162
- if self:HasPurchasedProductThisSession(productId) then
185
+ if self:HasPurchasedThisSession(Players.LocalPlayer, GameConfigAssetTypes.PRODUCT, productIdOrKey) then
163
186
  return Promise.resolved(true)
164
187
  end
165
188
 
166
- return self:PromiseLocalPlayerOwnsPass(gamepassId)
167
- :Then(function(owned)
168
- if owned then
169
- return owned
189
+ return self:PromisePlayerOwnership(Players.LocalPlayer, GameConfigAssetTypes.PASS, productIdOrKey)
190
+ :Then(function(owns)
191
+ if owns then
192
+ return true
193
+ else
194
+ return self:PromisePromptPurchase(Players.LocalPlayer, GameConfigAssetTypes.PRODUCT, productIdOrKey)
170
195
  end
171
-
172
- MarketplaceService:PromptProductPurchase(Players.LocalPlayer, productId)
173
- return owned
174
196
  end)
175
197
  end
176
198
 
@@ -8,102 +8,97 @@ local require = require(script.Parent.loader).load(script)
8
8
  local MarketplaceService = game:GetService("MarketplaceService")
9
9
  local Players = game:GetService("Players")
10
10
 
11
- local PlayerProductManagerBase = require("PlayerProductManagerBase")
11
+ local BaseObject = require("BaseObject")
12
12
  local PlayerProductManagerConstants = require("PlayerProductManagerConstants")
13
13
  local GameConfigServiceClient = require("GameConfigServiceClient")
14
- local Promise = require("Promise")
14
+ local GameConfigAssetTypes = require("GameConfigAssetTypes")
15
+ local PlayerMarketeer = require("PlayerMarketeer")
15
16
 
16
- local PlayerProductManagerClient = setmetatable({}, PlayerProductManagerBase)
17
+ local PlayerProductManagerClient = setmetatable({}, BaseObject)
17
18
  PlayerProductManagerClient.ClassName = "PlayerProductManagerClient"
18
19
  PlayerProductManagerClient.__index = PlayerProductManagerClient
19
20
 
20
21
  require("PromiseRemoteEventMixin"):Add(PlayerProductManagerClient, PlayerProductManagerConstants.REMOTE_EVENT_NAME)
21
22
 
22
23
  function PlayerProductManagerClient.new(obj, serviceBag)
23
- local self = setmetatable(PlayerProductManagerBase.new(obj), PlayerProductManagerClient)
24
+ local self = setmetatable(BaseObject.new(obj), PlayerProductManagerClient)
24
25
 
25
26
  self._serviceBag = assert(serviceBag, "No serviceBag")
26
27
  self._gameConfigServiceClient = self._serviceBag:GetService(GameConfigServiceClient)
27
28
 
28
-
29
29
  if self._obj == Players.LocalPlayer then
30
- self._pendingPassPromises = {}
30
+ self._marketeer = PlayerMarketeer.new(self._obj, self._gameConfigServiceClient:GetConfigPicker())
31
+ self._maid:GiveTask(self._marketeer)
31
32
 
32
- self:PromiseRemoteEvent():Then(function(remoteEvent)
33
- self:_setupRemoteEventLocal(remoteEvent)
34
- end)
33
+ self:_connectMarketplace()
35
34
 
36
- self._maid:GiveTask(MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, gamePassId, isPurchased)
37
- if player == self._obj then
38
- self:_handlePassFinished(player, gamePassId, isPurchased)
39
- end
40
- end))
35
+ -- Configure remote events
36
+ self:_replicateRemoteEventType(GameConfigAssetTypes.ASSET)
37
+ self:_replicateRemoteEventType(GameConfigAssetTypes.BUNDLE)
38
+ self:_replicateRemoteEventType(GameConfigAssetTypes.PASS)
39
+ self:_replicateRemoteEventType(GameConfigAssetTypes.PRODUCT)
41
40
  end
42
41
 
43
42
  return self
44
43
  end
45
44
 
46
- function PlayerProductManagerClient:PromptGamePassPurchase(gamePassId)
47
- assert(type(gamePassId) == "number", "Bad gamePassId")
48
- assert(self._obj == Players.LocalPlayer, "Can only prompt local player")
49
-
50
- if self._pendingPassPromises[gamePassId] then
51
- return self._maid:GivePromise(self._pendingPassPromises[gamePassId])
52
- end
45
+ --[=[
46
+ @return PlayerMarketeer
47
+ ]=]
48
+ function PlayerProductManagerClient:GetMarketeer()
49
+ return self._marketeer
50
+ end
53
51
 
54
- MarketplaceService:PromptGamePassPurchase(self._obj, gamePassId)
55
- local promise = Promise.new()
52
+ function PlayerProductManagerClient:_replicateRemoteEventType(assetType)
53
+ local tracker = self._marketeer:GetAssetTrackerOrError(assetType)
56
54
 
57
- self._pendingPassPromises[gamePassId] = promise
55
+ self._maid:GiveTask(tracker.PromptFinished:Connect(function(assetId, isPurchased)
56
+ self:PromiseRemoteEvent():Then(function(remoteEvent)
57
+ remoteEvent:FireServer(PlayerProductManagerConstants.NOTIFY_PROMPT_FINISHED, assetType, assetId, isPurchased)
58
+ end)
59
+ end))
60
+ end
58
61
 
59
- return self._maid:GivePromise(promise)
62
+ --[=[
63
+ Gets the current player
64
+ @return Player
65
+ ]=]
66
+ function PlayerProductManagerClient:GetPlayer()
67
+ return self._obj
60
68
  end
61
69
 
62
- function PlayerProductManagerClient:_setupRemoteEventLocal(remoteEvent)
63
- -- Gear and other assets
70
+ function PlayerProductManagerClient:_connectMarketplace()
71
+ -- Assets
64
72
  self._maid:GiveTask(MarketplaceService.PromptPurchaseFinished:Connect(function(player, assetId, isPurchased)
65
73
  if player == self._obj then
66
- remoteEvent:FireServer(PlayerProductManagerConstants.NOTIFY_PROMPT_FINISHED, assetId, isPurchased)
74
+ local tracker = self._marketeer:GetAssetTrackerOrError(GameConfigAssetTypes.ASSET)
75
+ tracker:HandlePurchaseEvent(assetId, isPurchased)
67
76
  end
68
77
  end))
69
78
 
70
79
  -- Products
71
- self._maid:GiveTask(MarketplaceService.PromptProductPurchaseFinished:Connect(function(userId, assetId, isPurchased)
80
+ self._maid:GiveTask(MarketplaceService.PromptProductPurchaseFinished:Connect(function(userId, productId, isPurchased)
72
81
  if self._obj.UserId == userId then
73
- remoteEvent:FireServer(PlayerProductManagerConstants.NOTIFY_PROMPT_FINISHED, assetId, isPurchased)
82
+ local tracker = self._marketeer:GetAssetTrackerOrError(GameConfigAssetTypes.PRODUCT)
83
+ tracker:HandlePurchaseEvent(productId, isPurchased)
74
84
  end
75
85
  end))
76
86
 
77
- -- Gamepasses
87
+ -- Game passes
78
88
  self._maid:GiveTask(MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, gamePassId, isPurchased)
79
89
  if player == self._obj then
80
- remoteEvent:FireServer(PlayerProductManagerConstants.NOTIFY_GAMEPASS_FINISHED, gamePassId, isPurchased)
90
+ local tracker = self._marketeer:GetAssetTrackerOrError(GameConfigAssetTypes.PASS)
91
+ tracker:HandlePurchaseEvent(gamePassId, isPurchased)
81
92
  end
82
93
  end))
83
- end
84
-
85
- -- For overrides
86
- function PlayerProductManagerClient:GetConfigPicker()
87
- return self._gameConfigServiceClient:GetConfigPicker()
88
- end
89
94
 
90
- function PlayerProductManagerClient:_handlePassFinished(player, gamePassId, isPurchased)
91
- assert(typeof(player) == "Instance", "Bad player")
92
- assert(type(gamePassId) == "number", "Bad gamePassId")
93
- assert(type(isPurchased) == "boolean", "Bad isPurchased")
94
-
95
- local promise = self._pendingPassPromises[gamePassId]
96
- if promise then
97
- if isPurchased then
98
- -- TODO: verify this on the server here
99
- -- Can we break cache?
100
- self:SetPlayerOwnsPass(gamePassId, true)
95
+ -- Bundles
96
+ self._maid:GiveTask(MarketplaceService.PromptBundlePurchaseFinished:Connect(function(player, bundleId, isPurchased)
97
+ if player == self._obj then
98
+ local tracker = self._marketeer:GetAssetTrackerOrError(GameConfigAssetTypes.BUNDLE)
99
+ tracker:HandlePurchaseEvent(bundleId, isPurchased)
101
100
  end
102
-
103
- self._pendingPassPromises[gamePassId] = nil
104
- promise:Resolve(isPurchased)
105
- end
101
+ end))
106
102
  end
107
103
 
108
-
109
104
  return PlayerProductManagerClient
@@ -1,4 +1,13 @@
1
1
  --[=[
2
+ This service provides an interface to purchase produces, assets, and other
3
+ marketplace items. This listens to events, handles requests between server and
4
+ client, and takes in both assetKeys from GameConfigService, as well as
5
+ assetIds.
6
+
7
+ See [GameProductServiceClient] for the client equivalent. The API surface should be
8
+ effectively the same between the two.
9
+
10
+ @server
2
11
  @class GameProductService
3
12
  ]=]
4
13
 
@@ -7,11 +16,17 @@ local require = require(script.Parent.loader).load(script)
7
16
  local Players = game:GetService("Players")
8
17
  local MarketplaceService = game:GetService("MarketplaceService")
9
18
 
10
- local GameProductServiceBase = require("GameProductServiceBase")
11
19
  local Maid = require("Maid")
20
+ local GameProductServiceHelper = require("GameProductServiceHelper")
21
+ local GameConfigAssetTypeUtils = require("GameConfigAssetTypeUtils")
12
22
 
13
- local GameProductService = GameProductServiceBase.new()
23
+ local GameProductService = {}
24
+ GameProductService.ServiceName = "GameProductService"
14
25
 
26
+ --[=[
27
+ Initializes the service. Should be done via [ServiceBag]
28
+ @param serviceBag ServiceBag
29
+ ]=]
15
30
  function GameProductService:Init(serviceBag)
16
31
  assert(not self._serviceBag, "Already initialized")
17
32
 
@@ -23,21 +38,93 @@ function GameProductService:Init(serviceBag)
23
38
 
24
39
  -- Internal
25
40
  self._binders = self._serviceBag:GetService(require("GameProductBindersServer"))
41
+
42
+ -- Configure
43
+ self._helper = GameProductServiceHelper.new(self._binders.PlayerProductManager)
44
+ self._maid:GiveTask(self._helper)
26
45
  end
27
46
 
47
+ --[=[
48
+ Starts the service. Should be done via [ServiceBag]
49
+ ]=]
28
50
  function GameProductService:Start()
51
+ -- TODO: Avoid binding this unless explicitly asked to
52
+ -- TODO: Provide receipt processing API surface
53
+
29
54
  MarketplaceService.ProcessReceipt = function(...)
30
55
  return self:_processReceipt(...)
31
56
  end
32
57
 
33
58
  self._maid:GiveTask(function()
34
- -- This might be unsafe
35
- MarketplaceService.ProcessReceipt = nil
59
+ task.spawn(function()
60
+ -- This might be unsafe
61
+ MarketplaceService.ProcessReceipt = nil
62
+ end)
36
63
  end)
37
64
  end
38
65
 
39
- function GameProductService:GetPlayerProductManagerBinder()
40
- return self._binders.PlayerProductManager
66
+ --[=[
67
+ Returns true if item has been purchased this session
68
+
69
+ @param player Player
70
+ @param assetType GameConfigAssetType
71
+ @param idOrKey string | number
72
+ @return boolean
73
+ ]=]
74
+ function GameProductService:HasPlayerPurchasedThisSession(player, assetType, idOrKey)
75
+ assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
76
+ assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
77
+ assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
78
+
79
+ return self._helper:HasPlayerPurchasedThisSession(player, assetType, idOrKey)
80
+ end
81
+
82
+ --[=[
83
+ Prompts the user to purchase the asset, and returns true if purchased
84
+
85
+ @param player Player
86
+ @param assetType GameConfigAssetType
87
+ @param idOrKey string | number
88
+ @return boolean
89
+ ]=]
90
+ function GameProductService:PromisePlayerPromptPurchase(player, assetType, idOrKey)
91
+ assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
92
+ assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
93
+ assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
94
+
95
+ return self._helper:PromisePromptPurchase(player, assetType, idOrKey)
96
+ end
97
+
98
+ --[=[
99
+ Returns true if item has been purchased this session
100
+
101
+ @param player Player
102
+ @param assetType GameConfigAssetType
103
+ @param idOrKey string | number
104
+ @return Promise<boolean>
105
+ ]=]
106
+ function GameProductService:PromisePlayerOwnership(player, assetType, idOrKey)
107
+ assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
108
+ assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
109
+ assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
110
+
111
+ return self._helper:PromisePlayerOwnership(player, assetType, idOrKey)
112
+ end
113
+
114
+ --[=[
115
+ Observes if the player owns this cloud asset or not
116
+
117
+ @param player Player
118
+ @param assetType GameConfigAssetType
119
+ @param idOrKey string | number
120
+ @return Observable<boolean>
121
+ ]=]
122
+ function GameProductService:ObservePlayerOwnership(player, assetType, idOrKey)
123
+ assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
124
+ assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
125
+ assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
126
+
127
+ return self._helper:ObservePlayerOwnership(player, assetType, idOrKey)
41
128
  end
42
129
 
43
130
  function GameProductService:_processReceipt(receiptInfo)