@bitmagic/cli 0.1.83-dev.1 → 0.1.83-dev.2
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/README.md +69 -0
- package/assets/skills/publishing-to-steam/SKILL.md +218 -0
- package/dist/assets/vxl-summary.d.ts +6 -20
- package/dist/assets/vxl-summary.js +9 -100
- package/dist/assets/vxl-summary.js.map +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/check.js +5 -0
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/forge.js +1 -1
- package/dist/commands/forge.js.map +1 -1
- package/dist/commands/ios.js +88 -136
- package/dist/commands/ios.js.map +1 -1
- package/dist/commands/steam-build.d.ts +67 -0
- package/dist/commands/steam-build.js +286 -0
- package/dist/commands/steam-build.js.map +1 -0
- package/dist/commands/steam-portal.d.ts +1 -0
- package/dist/commands/steam-portal.js +319 -0
- package/dist/commands/steam-portal.js.map +1 -0
- package/dist/commands/steam-upload.d.ts +37 -0
- package/dist/commands/steam-upload.js +172 -0
- package/dist/commands/steam-upload.js.map +1 -0
- package/dist/commands/steam.d.ts +1 -0
- package/dist/commands/steam.js +502 -0
- package/dist/commands/steam.js.map +1 -0
- package/dist/commands/verify.js +6 -0
- package/dist/commands/verify.js.map +1 -1
- package/dist/editor/asset-viewer.js +5 -4
- package/dist/editor/asset-viewer.js.map +1 -1
- package/dist/editor/server.js +1 -1
- package/dist/editor/server.js.map +1 -1
- package/dist/editor/shell-page.js +3 -3
- package/dist/forge/browser-host.d.ts +1 -1
- package/dist/ios/native-overrides.d.ts +27 -0
- package/dist/ios/native-overrides.js +87 -0
- package/dist/ios/native-overrides.js.map +1 -0
- package/dist/ios/record-validate.d.ts +1 -33
- package/dist/ios/record-validate.js +2 -100
- package/dist/ios/record-validate.js.map +1 -1
- package/dist/ios/store-bridge.d.ts +26 -0
- package/dist/ios/store-bridge.js +224 -0
- package/dist/ios/store-bridge.js.map +1 -0
- package/dist/ios/storekit-config.d.ts +15 -0
- package/dist/ios/storekit-config.js +106 -0
- package/dist/ios/storekit-config.js.map +1 -0
- package/dist/ios/xcode-template.d.ts +12 -3
- package/dist/ios/xcode-template.js +158 -27
- package/dist/ios/xcode-template.js.map +1 -1
- package/dist/package/collect-urls.d.ts +1 -1
- package/dist/package/collect-urls.js +10 -2
- package/dist/package/collect-urls.js.map +1 -1
- package/dist/package/offline-payload.d.ts +35 -0
- package/dist/package/offline-payload.js +142 -0
- package/dist/package/offline-payload.js.map +1 -0
- package/dist/package/packaged-check.d.ts +39 -0
- package/dist/package/packaged-check.js +104 -0
- package/dist/package/packaged-check.js.map +1 -0
- package/dist/package/transform.d.ts +4 -4
- package/dist/package/transform.js +3 -3
- package/dist/project/published-three.d.ts +14 -0
- package/dist/project/published-three.js +93 -0
- package/dist/project/published-three.js.map +1 -0
- package/dist/publish/bundle.js +6 -0
- package/dist/publish/bundle.js.map +1 -1
- package/dist/scaffold/project-files.js +5 -2
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/scaffold/project.js +2 -0
- package/dist/scaffold/project.js.map +1 -1
- package/dist/scaffold/skill-files.js +3 -1
- package/dist/scaffold/skill-files.js.map +1 -1
- package/dist/scaffold/steam-skill-files.d.ts +2 -0
- package/dist/scaffold/steam-skill-files.js +7 -0
- package/dist/scaffold/steam-skill-files.js.map +1 -0
- package/dist/steam/art-generate.d.ts +55 -0
- package/dist/steam/art-generate.js +173 -0
- package/dist/steam/art-generate.js.map +1 -0
- package/dist/steam/art-render.d.ts +25 -0
- package/dist/steam/art-render.js +157 -0
- package/dist/steam/art-render.js.map +1 -0
- package/dist/steam/art-use.d.ts +20 -0
- package/dist/steam/art-use.js +84 -0
- package/dist/steam/art-use.js.map +1 -0
- package/dist/steam/art.d.ts +98 -0
- package/dist/steam/art.js +243 -0
- package/dist/steam/art.js.map +1 -0
- package/dist/steam/check.d.ts +21 -0
- package/dist/steam/check.js +113 -0
- package/dist/steam/check.js.map +1 -0
- package/dist/steam/config.d.ts +57 -0
- package/dist/steam/config.js +145 -0
- package/dist/steam/config.js.map +1 -0
- package/dist/steam/electron-cache.d.ts +11 -0
- package/dist/steam/electron-cache.js +105 -0
- package/dist/steam/electron-cache.js.map +1 -0
- package/dist/steam/fuses.d.ts +16 -0
- package/dist/steam/fuses.js +75 -0
- package/dist/steam/fuses.js.map +1 -0
- package/dist/steam/icon-files.d.ts +16 -0
- package/dist/steam/icon-files.js +70 -0
- package/dist/steam/icon-files.js.map +1 -0
- package/dist/steam/image-info.d.ts +16 -0
- package/dist/steam/image-info.js +89 -0
- package/dist/steam/image-info.js.map +1 -0
- package/dist/steam/launch-check.d.ts +12 -0
- package/dist/steam/launch-check.js +101 -0
- package/dist/steam/launch-check.js.map +1 -0
- package/dist/steam/media.d.ts +48 -0
- package/dist/steam/media.js +151 -0
- package/dist/steam/media.js.map +1 -0
- package/dist/steam/package-macos.d.ts +52 -0
- package/dist/steam/package-macos.js +268 -0
- package/dist/steam/package-macos.js.map +1 -0
- package/dist/steam/package-windows.d.ts +24 -0
- package/dist/steam/package-windows.js +84 -0
- package/dist/steam/package-windows.js.map +1 -0
- package/dist/steam/paths.d.ts +37 -0
- package/dist/steam/paths.js +45 -0
- package/dist/steam/paths.js.map +1 -0
- package/dist/steam/playtest-shots.d.ts +18 -0
- package/dist/steam/playtest-shots.js +83 -0
- package/dist/steam/playtest-shots.js.map +1 -0
- package/dist/steam/portal-recipes.d.ts +25 -0
- package/dist/steam/portal-recipes.js +61 -0
- package/dist/steam/portal-recipes.js.map +1 -0
- package/dist/steam/portal.d.ts +58 -0
- package/dist/steam/portal.js +340 -0
- package/dist/steam/portal.js.map +1 -0
- package/dist/steam/raster.d.ts +59 -0
- package/dist/steam/raster.js +285 -0
- package/dist/steam/raster.js.map +1 -0
- package/dist/steam/shell-files.d.ts +17 -0
- package/dist/steam/shell-files.js +268 -0
- package/dist/steam/shell-files.js.map +1 -0
- package/dist/steam/specs.d.ts +61 -0
- package/dist/steam/specs.js +103 -0
- package/dist/steam/specs.js.map +1 -0
- package/dist/steam/steampipe.d.ts +46 -0
- package/dist/steam/steampipe.js +170 -0
- package/dist/steam/steampipe.js.map +1 -0
- package/dist/steam/store-copy.d.ts +89 -0
- package/dist/steam/store-copy.js +221 -0
- package/dist/steam/store-copy.js.map +1 -0
- package/dist/steam/tags.d.ts +9 -0
- package/dist/steam/tags.js +49 -0
- package/dist/steam/tags.js.map +1 -0
- package/dist/steam/trailer.d.ts +34 -0
- package/dist/steam/trailer.js +141 -0
- package/dist/steam/trailer.js.map +1 -0
- package/dist/verify/browser.d.ts +11 -0
- package/dist/verify/browser.js +195 -152
- package/dist/verify/browser.js.map +1 -1
- package/dist/verify/classify.d.ts +11 -2
- package/dist/verify/classify.js +16 -4
- package/dist/verify/classify.js.map +1 -1
- package/dist/verify/mobile-parity.d.ts +1 -1
- package/dist/verify/mobile-parity.js +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell's App Store bridge (§13 of docs/ios-export-design.md): StoreKit 2 behind one
|
|
3
|
+
* `WKScriptMessageHandlerWithReply`, so the page's `postMessage` returns a promise and no request
|
|
4
|
+
* ids are needed. The engine side is `game/src/engine/store/NativeStoreBackend.ts`; the message
|
|
5
|
+
* shapes below are the contract between them, so change both or neither.
|
|
6
|
+
*
|
|
7
|
+
* Ops (page → app, each answered once):
|
|
8
|
+
* status { canMakePayments }
|
|
9
|
+
* products { ids } → { products, missing }
|
|
10
|
+
* purchase { productId, quantity?, appAccountToken? }
|
|
11
|
+
* → { status: purchased | pending | cancelled | unverified | failed, transaction?, reason? }
|
|
12
|
+
* unfinished {} → { transactions }
|
|
13
|
+
* finish { transactionId } → { finished }
|
|
14
|
+
* Event (app → page): a `bmstore:transaction` window event whenever StoreKit delivers a purchase
|
|
15
|
+
* outside a purchase() call. It carries nothing; the page answers it by asking for `unfinished`,
|
|
16
|
+
* so a page that was not listening yet loses nothing.
|
|
17
|
+
*
|
|
18
|
+
* A verified purchase is never finished here. The engine records the credit first and then asks
|
|
19
|
+
* for `finish`; until then StoreKit keeps re-delivering the transaction, which is what makes an
|
|
20
|
+
* app killed between payment and crediting safe.
|
|
21
|
+
*
|
|
22
|
+
* Same literal rules as xcode-template.ts: no Swift `\(...)` interpolation, no backticks.
|
|
23
|
+
*/
|
|
24
|
+
export declare const STORE_HANDLER_NAME = "bmStore";
|
|
25
|
+
export declare const STORE_TRANSACTION_EVENT = "bmstore:transaction";
|
|
26
|
+
export declare const STORE_BRIDGE_SWIFT = "import Foundation\nimport StoreKit\nimport WebKit\n\n/// App Store purchases for the game page. See the ops table in the CLI's store-bridge.ts.\nfinal class StoreBridge: NSObject, WKScriptMessageHandlerWithReply {\n static let handlerName = \"bmStore\"\n static let transactionEvent = \"bmstore:transaction\"\n\n weak var webView: WKWebView?\n private var updatesTask: Task<Void, Never>?\n /// Verified, not yet finished transactions, by id \u2014 so finish never has to search.\n private var pending: [UInt64: Transaction] = [:]\n\n deinit {\n updatesTask?.cancel()\n }\n\n /// Listen for purchases that complete outside a purchase() call: Ask to Buy approvals,\n /// payment-method fixes, a purchase interrupted by the app being killed. Apple's guidance is\n /// to start this at launch, before any purchase, or such transactions can be missed.\n @MainActor\n func start() {\n updatesTask = Task { [weak self] in\n for await result in Transaction.updates {\n self?.received(result)\n }\n }\n }\n\n @MainActor\n private func received(_ result: VerificationResult<Transaction>) {\n guard case .verified(let transaction) = result else { return }\n pending[transaction.id] = transaction\n let script = \"window.dispatchEvent(new CustomEvent('\" + Self.transactionEvent + \"'))\"\n webView?.evaluateJavaScript(script, completionHandler: nil)\n }\n\n func userContentController(\n _ userContentController: WKUserContentController,\n didReceive message: WKScriptMessage,\n replyHandler: @escaping (Any?, String?) -> Void\n ) {\n guard let body = message.body as? [String: Any], let op = body[\"op\"] as? String else {\n replyHandler(nil, \"A store request needs an op.\")\n return\n }\n Task { @MainActor in\n do {\n let reply = try await self.handle(op: op, body: body)\n replyHandler(reply, nil)\n } catch {\n replyHandler(nil, String(describing: error))\n }\n }\n }\n\n @MainActor\n private func handle(op: String, body: [String: Any]) async throws -> Any {\n switch op {\n case \"status\":\n return [\"canMakePayments\": AppStore.canMakePayments]\n case \"products\":\n let ids = (body[\"ids\"] as? [String]) ?? []\n let products = try await Product.products(for: ids)\n let found = Set(products.map { $0.id })\n return [\n \"products\": products.map { Self.productInfo($0) },\n \"missing\": ids.filter { !found.contains($0) },\n ]\n case \"purchase\":\n return try await purchase(body)\n case \"unfinished\":\n var transactions: [[String: Any]] = []\n for await result in Transaction.unfinished {\n guard case .verified(let transaction) = result else { continue }\n pending[transaction.id] = transaction\n transactions.append(Self.transactionInfo(transaction, jws: result.jwsRepresentation))\n }\n return [\"transactions\": transactions]\n case \"finish\":\n guard let text = body[\"transactionId\"] as? String, let id = UInt64(text) else {\n throw StoreBridgeError.badRequest(\"finish needs a transactionId string\")\n }\n return [\"finished\": await finish(id)]\n default:\n throw StoreBridgeError.badRequest(\"Unknown store op \" + op)\n }\n }\n\n @MainActor\n private func purchase(_ body: [String: Any]) async throws -> Any {\n guard let productId = body[\"productId\"] as? String else {\n throw StoreBridgeError.badRequest(\"purchase needs a productId\")\n }\n guard let product = try await Product.products(for: [productId]).first else {\n return [\n \"status\": \"failed\",\n \"reason\": \"The App Store did not return the product \" + productId + \".\",\n ]\n }\n var options: Set<Product.PurchaseOption> = []\n if let quantity = body[\"quantity\"] as? Int, quantity > 1 {\n options.insert(.quantity(quantity))\n }\n if let token = body[\"appAccountToken\"] as? String, let uuid = UUID(uuidString: token) {\n options.insert(.appAccountToken(uuid))\n }\n let result = try await product.purchase(options: options)\n switch result {\n case .success(let verification):\n switch verification {\n case .verified(let transaction):\n pending[transaction.id] = transaction\n return [\n \"status\": \"purchased\",\n \"transaction\": Self.transactionInfo(transaction, jws: verification.jwsRepresentation),\n ]\n case .unverified(_, let error):\n return [\"status\": \"unverified\", \"reason\": String(describing: error)]\n }\n case .userCancelled:\n return [\"status\": \"cancelled\"]\n case .pending:\n return [\"status\": \"pending\"]\n @unknown default:\n return [\"status\": \"failed\", \"reason\": \"StoreKit returned an unknown purchase result.\"]\n }\n }\n\n @MainActor\n private func finish(_ id: UInt64) async -> Bool {\n if let transaction = pending.removeValue(forKey: id) {\n await transaction.finish()\n return true\n }\n for await result in Transaction.unfinished {\n if case .verified(let transaction) = result, transaction.id == id {\n await transaction.finish()\n return true\n }\n }\n return false\n }\n\n private static func typeName(_ type: Product.ProductType) -> String {\n if type == .consumable { return \"consumable\" }\n if type == .nonConsumable { return \"nonConsumable\" }\n if type == .autoRenewable { return \"autoRenewable\" }\n if type == .nonRenewable { return \"nonRenewable\" }\n return \"other\"\n }\n\n private static func productInfo(_ product: Product) -> [String: Any] {\n return [\n \"id\": product.id,\n \"type\": typeName(product.type),\n \"displayName\": product.displayName,\n \"description\": product.description,\n \"displayPrice\": product.displayPrice,\n \"price\": NSDecimalNumber(decimal: product.price).doubleValue,\n \"currencyCode\": product.priceFormatStyle.currencyCode,\n ]\n }\n\n /// Ids go to the page as strings: a transaction id is a UInt64 and need not fit a double.\n private static func transactionInfo(_ transaction: Transaction, jws: String) -> [String: Any] {\n var info: [String: Any] = [\n \"transactionId\": String(transaction.id),\n \"originalTransactionId\": String(transaction.originalID),\n \"productId\": transaction.productID,\n \"productType\": typeName(transaction.productType),\n \"quantity\": transaction.purchasedQuantity,\n \"purchaseDate\": transaction.purchaseDate.timeIntervalSince1970 * 1000,\n \"revoked\": transaction.revocationDate != nil,\n \"jws\": jws,\n ]\n if #available(iOS 16.0, *) {\n info[\"environment\"] = transaction.environment.rawValue\n }\n if let token = transaction.appAccountToken {\n info[\"appAccountToken\"] = token.uuidString\n }\n return info\n }\n}\n\nenum StoreBridgeError: Error, CustomStringConvertible {\n case badRequest(String)\n\n var description: String {\n switch self {\n case .badRequest(let message):\n return message\n }\n }\n}\n";
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell's App Store bridge (§13 of docs/ios-export-design.md): StoreKit 2 behind one
|
|
3
|
+
* `WKScriptMessageHandlerWithReply`, so the page's `postMessage` returns a promise and no request
|
|
4
|
+
* ids are needed. The engine side is `game/src/engine/store/NativeStoreBackend.ts`; the message
|
|
5
|
+
* shapes below are the contract between them, so change both or neither.
|
|
6
|
+
*
|
|
7
|
+
* Ops (page → app, each answered once):
|
|
8
|
+
* status { canMakePayments }
|
|
9
|
+
* products { ids } → { products, missing }
|
|
10
|
+
* purchase { productId, quantity?, appAccountToken? }
|
|
11
|
+
* → { status: purchased | pending | cancelled | unverified | failed, transaction?, reason? }
|
|
12
|
+
* unfinished {} → { transactions }
|
|
13
|
+
* finish { transactionId } → { finished }
|
|
14
|
+
* Event (app → page): a `bmstore:transaction` window event whenever StoreKit delivers a purchase
|
|
15
|
+
* outside a purchase() call. It carries nothing; the page answers it by asking for `unfinished`,
|
|
16
|
+
* so a page that was not listening yet loses nothing.
|
|
17
|
+
*
|
|
18
|
+
* A verified purchase is never finished here. The engine records the credit first and then asks
|
|
19
|
+
* for `finish`; until then StoreKit keeps re-delivering the transaction, which is what makes an
|
|
20
|
+
* app killed between payment and crediting safe.
|
|
21
|
+
*
|
|
22
|
+
* Same literal rules as xcode-template.ts: no Swift `\(...)` interpolation, no backticks.
|
|
23
|
+
*/
|
|
24
|
+
export const STORE_HANDLER_NAME = 'bmStore';
|
|
25
|
+
export const STORE_TRANSACTION_EVENT = 'bmstore:transaction';
|
|
26
|
+
export const STORE_BRIDGE_SWIFT = `import Foundation
|
|
27
|
+
import StoreKit
|
|
28
|
+
import WebKit
|
|
29
|
+
|
|
30
|
+
/// App Store purchases for the game page. See the ops table in the CLI's store-bridge.ts.
|
|
31
|
+
final class StoreBridge: NSObject, WKScriptMessageHandlerWithReply {
|
|
32
|
+
static let handlerName = "${STORE_HANDLER_NAME}"
|
|
33
|
+
static let transactionEvent = "${STORE_TRANSACTION_EVENT}"
|
|
34
|
+
|
|
35
|
+
weak var webView: WKWebView?
|
|
36
|
+
private var updatesTask: Task<Void, Never>?
|
|
37
|
+
/// Verified, not yet finished transactions, by id — so finish never has to search.
|
|
38
|
+
private var pending: [UInt64: Transaction] = [:]
|
|
39
|
+
|
|
40
|
+
deinit {
|
|
41
|
+
updatesTask?.cancel()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Listen for purchases that complete outside a purchase() call: Ask to Buy approvals,
|
|
45
|
+
/// payment-method fixes, a purchase interrupted by the app being killed. Apple's guidance is
|
|
46
|
+
/// to start this at launch, before any purchase, or such transactions can be missed.
|
|
47
|
+
@MainActor
|
|
48
|
+
func start() {
|
|
49
|
+
updatesTask = Task { [weak self] in
|
|
50
|
+
for await result in Transaction.updates {
|
|
51
|
+
self?.received(result)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@MainActor
|
|
57
|
+
private func received(_ result: VerificationResult<Transaction>) {
|
|
58
|
+
guard case .verified(let transaction) = result else { return }
|
|
59
|
+
pending[transaction.id] = transaction
|
|
60
|
+
let script = "window.dispatchEvent(new CustomEvent('" + Self.transactionEvent + "'))"
|
|
61
|
+
webView?.evaluateJavaScript(script, completionHandler: nil)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func userContentController(
|
|
65
|
+
_ userContentController: WKUserContentController,
|
|
66
|
+
didReceive message: WKScriptMessage,
|
|
67
|
+
replyHandler: @escaping (Any?, String?) -> Void
|
|
68
|
+
) {
|
|
69
|
+
guard let body = message.body as? [String: Any], let op = body["op"] as? String else {
|
|
70
|
+
replyHandler(nil, "A store request needs an op.")
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
Task { @MainActor in
|
|
74
|
+
do {
|
|
75
|
+
let reply = try await self.handle(op: op, body: body)
|
|
76
|
+
replyHandler(reply, nil)
|
|
77
|
+
} catch {
|
|
78
|
+
replyHandler(nil, String(describing: error))
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@MainActor
|
|
84
|
+
private func handle(op: String, body: [String: Any]) async throws -> Any {
|
|
85
|
+
switch op {
|
|
86
|
+
case "status":
|
|
87
|
+
return ["canMakePayments": AppStore.canMakePayments]
|
|
88
|
+
case "products":
|
|
89
|
+
let ids = (body["ids"] as? [String]) ?? []
|
|
90
|
+
let products = try await Product.products(for: ids)
|
|
91
|
+
let found = Set(products.map { $0.id })
|
|
92
|
+
return [
|
|
93
|
+
"products": products.map { Self.productInfo($0) },
|
|
94
|
+
"missing": ids.filter { !found.contains($0) },
|
|
95
|
+
]
|
|
96
|
+
case "purchase":
|
|
97
|
+
return try await purchase(body)
|
|
98
|
+
case "unfinished":
|
|
99
|
+
var transactions: [[String: Any]] = []
|
|
100
|
+
for await result in Transaction.unfinished {
|
|
101
|
+
guard case .verified(let transaction) = result else { continue }
|
|
102
|
+
pending[transaction.id] = transaction
|
|
103
|
+
transactions.append(Self.transactionInfo(transaction, jws: result.jwsRepresentation))
|
|
104
|
+
}
|
|
105
|
+
return ["transactions": transactions]
|
|
106
|
+
case "finish":
|
|
107
|
+
guard let text = body["transactionId"] as? String, let id = UInt64(text) else {
|
|
108
|
+
throw StoreBridgeError.badRequest("finish needs a transactionId string")
|
|
109
|
+
}
|
|
110
|
+
return ["finished": await finish(id)]
|
|
111
|
+
default:
|
|
112
|
+
throw StoreBridgeError.badRequest("Unknown store op " + op)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@MainActor
|
|
117
|
+
private func purchase(_ body: [String: Any]) async throws -> Any {
|
|
118
|
+
guard let productId = body["productId"] as? String else {
|
|
119
|
+
throw StoreBridgeError.badRequest("purchase needs a productId")
|
|
120
|
+
}
|
|
121
|
+
guard let product = try await Product.products(for: [productId]).first else {
|
|
122
|
+
return [
|
|
123
|
+
"status": "failed",
|
|
124
|
+
"reason": "The App Store did not return the product " + productId + ".",
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
var options: Set<Product.PurchaseOption> = []
|
|
128
|
+
if let quantity = body["quantity"] as? Int, quantity > 1 {
|
|
129
|
+
options.insert(.quantity(quantity))
|
|
130
|
+
}
|
|
131
|
+
if let token = body["appAccountToken"] as? String, let uuid = UUID(uuidString: token) {
|
|
132
|
+
options.insert(.appAccountToken(uuid))
|
|
133
|
+
}
|
|
134
|
+
let result = try await product.purchase(options: options)
|
|
135
|
+
switch result {
|
|
136
|
+
case .success(let verification):
|
|
137
|
+
switch verification {
|
|
138
|
+
case .verified(let transaction):
|
|
139
|
+
pending[transaction.id] = transaction
|
|
140
|
+
return [
|
|
141
|
+
"status": "purchased",
|
|
142
|
+
"transaction": Self.transactionInfo(transaction, jws: verification.jwsRepresentation),
|
|
143
|
+
]
|
|
144
|
+
case .unverified(_, let error):
|
|
145
|
+
return ["status": "unverified", "reason": String(describing: error)]
|
|
146
|
+
}
|
|
147
|
+
case .userCancelled:
|
|
148
|
+
return ["status": "cancelled"]
|
|
149
|
+
case .pending:
|
|
150
|
+
return ["status": "pending"]
|
|
151
|
+
@unknown default:
|
|
152
|
+
return ["status": "failed", "reason": "StoreKit returned an unknown purchase result."]
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@MainActor
|
|
157
|
+
private func finish(_ id: UInt64) async -> Bool {
|
|
158
|
+
if let transaction = pending.removeValue(forKey: id) {
|
|
159
|
+
await transaction.finish()
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
162
|
+
for await result in Transaction.unfinished {
|
|
163
|
+
if case .verified(let transaction) = result, transaction.id == id {
|
|
164
|
+
await transaction.finish()
|
|
165
|
+
return true
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private static func typeName(_ type: Product.ProductType) -> String {
|
|
172
|
+
if type == .consumable { return "consumable" }
|
|
173
|
+
if type == .nonConsumable { return "nonConsumable" }
|
|
174
|
+
if type == .autoRenewable { return "autoRenewable" }
|
|
175
|
+
if type == .nonRenewable { return "nonRenewable" }
|
|
176
|
+
return "other"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private static func productInfo(_ product: Product) -> [String: Any] {
|
|
180
|
+
return [
|
|
181
|
+
"id": product.id,
|
|
182
|
+
"type": typeName(product.type),
|
|
183
|
+
"displayName": product.displayName,
|
|
184
|
+
"description": product.description,
|
|
185
|
+
"displayPrice": product.displayPrice,
|
|
186
|
+
"price": NSDecimalNumber(decimal: product.price).doubleValue,
|
|
187
|
+
"currencyCode": product.priceFormatStyle.currencyCode,
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Ids go to the page as strings: a transaction id is a UInt64 and need not fit a double.
|
|
192
|
+
private static func transactionInfo(_ transaction: Transaction, jws: String) -> [String: Any] {
|
|
193
|
+
var info: [String: Any] = [
|
|
194
|
+
"transactionId": String(transaction.id),
|
|
195
|
+
"originalTransactionId": String(transaction.originalID),
|
|
196
|
+
"productId": transaction.productID,
|
|
197
|
+
"productType": typeName(transaction.productType),
|
|
198
|
+
"quantity": transaction.purchasedQuantity,
|
|
199
|
+
"purchaseDate": transaction.purchaseDate.timeIntervalSince1970 * 1000,
|
|
200
|
+
"revoked": transaction.revocationDate != nil,
|
|
201
|
+
"jws": jws,
|
|
202
|
+
]
|
|
203
|
+
if #available(iOS 16.0, *) {
|
|
204
|
+
info["environment"] = transaction.environment.rawValue
|
|
205
|
+
}
|
|
206
|
+
if let token = transaction.appAccountToken {
|
|
207
|
+
info["appAccountToken"] = token.uuidString
|
|
208
|
+
}
|
|
209
|
+
return info
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
enum StoreBridgeError: Error, CustomStringConvertible {
|
|
214
|
+
case badRequest(String)
|
|
215
|
+
|
|
216
|
+
var description: String {
|
|
217
|
+
switch self {
|
|
218
|
+
case .badRequest(let message):
|
|
219
|
+
return message
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
`;
|
|
224
|
+
//# sourceMappingURL=store-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-bridge.js","sourceRoot":"","sources":["../../src/ios/store-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAC5C,MAAM,CAAC,MAAM,uBAAuB,GAAG,qBAAqB,CAAC;AAE7D,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;gCAMF,kBAAkB;qCACb,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8L3D,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface StoreKitProductInput {
|
|
2
|
+
id: string;
|
|
3
|
+
/** Test price, e.g. "0.99". Defaults to 0.99. */
|
|
4
|
+
price?: string;
|
|
5
|
+
/** Test display name. Defaults to the id. */
|
|
6
|
+
name?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface StoreKitUpsertResult {
|
|
9
|
+
created: boolean;
|
|
10
|
+
added: string[];
|
|
11
|
+
alreadyListed: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function validateProductId(id: string): void;
|
|
14
|
+
/** Add the consumables the file does not list yet; create the file when there is none. */
|
|
15
|
+
export declare function upsertStoreKitConsumables(filePath: string, products: readonly StoreKitProductInput[]): StoreKitUpsertResult;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ios/StoreKit.storekit` — Xcode's local StoreKit test catalogue (§13 of
|
|
3
|
+
* docs/ios-export-design.md). With it, a run started from Xcode answers products and purchases
|
|
4
|
+
* from this file instead of the App Store, so a shop can be exercised in the Simulator before the
|
|
5
|
+
* creator has set anything up in App Store Connect.
|
|
6
|
+
*
|
|
7
|
+
* The file is the creator's: `bitmagic ios storekit` only ever ADDS consumables it does not
|
|
8
|
+
* already list, and everything else (prices, names, other product types edited in Xcode) is kept
|
|
9
|
+
* verbatim. Prices and names here are test values; the real ones come from App Store Connect.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from 'crypto';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { CliError } from '../errors.js';
|
|
15
|
+
/** Apple's rule for a product id: letters, digits, underscores and periods. */
|
|
16
|
+
const PRODUCT_ID = /^[A-Za-z0-9._]+$/;
|
|
17
|
+
/** Stable 8-hex ids, so regenerating the same product writes the same file. */
|
|
18
|
+
function internalId(seed) {
|
|
19
|
+
return createHash('sha256').update(seed).digest('hex').slice(0, 8).toUpperCase();
|
|
20
|
+
}
|
|
21
|
+
function emptyCatalogue(seed) {
|
|
22
|
+
return {
|
|
23
|
+
identifier: internalId(`catalogue:${seed}`),
|
|
24
|
+
nonRenewingSubscriptions: [],
|
|
25
|
+
products: [],
|
|
26
|
+
settings: {
|
|
27
|
+
_failTransactionsEnabled: false,
|
|
28
|
+
_locale: 'en_US',
|
|
29
|
+
_storefront: 'USA',
|
|
30
|
+
},
|
|
31
|
+
subscriptionGroups: [],
|
|
32
|
+
version: { major: 2, minor: 0 },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function consumable(input) {
|
|
36
|
+
const name = input.name ?? input.id;
|
|
37
|
+
return {
|
|
38
|
+
displayPrice: input.price ?? '0.99',
|
|
39
|
+
familyShareable: false,
|
|
40
|
+
internalID: internalId(`product:${input.id}`),
|
|
41
|
+
localizations: [{ description: name, displayName: name, locale: 'en_US' }],
|
|
42
|
+
productID: input.id,
|
|
43
|
+
referenceName: name,
|
|
44
|
+
type: 'Consumable',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function validateProductId(id) {
|
|
48
|
+
if (!PRODUCT_ID.test(id)) {
|
|
49
|
+
throw new CliError(`"${id}" is not a valid App Store product id: use letters, digits, underscores and periods `
|
|
50
|
+
+ '(e.g. gems_100 or com.studio.game.gems100).', 2);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function validatePrice(price) {
|
|
54
|
+
if (!/^\d+(\.\d{1,2})?$/.test(price)) {
|
|
55
|
+
throw new CliError(`--price must be a plain amount such as 0.99, not "${price}".`, 2);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Add the consumables the file does not list yet; create the file when there is none. */
|
|
59
|
+
export function upsertStoreKitConsumables(filePath, products) {
|
|
60
|
+
for (const product of products) {
|
|
61
|
+
validateProductId(product.id);
|
|
62
|
+
if (product.price !== undefined)
|
|
63
|
+
validatePrice(product.price);
|
|
64
|
+
}
|
|
65
|
+
const created = !fs.existsSync(filePath);
|
|
66
|
+
let catalogue;
|
|
67
|
+
if (created) {
|
|
68
|
+
catalogue = emptyCatalogue(products.map((p) => p.id).join(','));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
throw new CliError(`${filePath} is not valid JSON (${err.message}). Fix or delete it.`, 3);
|
|
77
|
+
}
|
|
78
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
79
|
+
throw new CliError(`${filePath} is not a StoreKit configuration file. Fix or delete it.`, 3);
|
|
80
|
+
}
|
|
81
|
+
catalogue = parsed;
|
|
82
|
+
}
|
|
83
|
+
const list = Array.isArray(catalogue.products) ? catalogue.products : [];
|
|
84
|
+
const listed = new Set(list.flatMap((entry) => {
|
|
85
|
+
const id = entry?.productID;
|
|
86
|
+
return typeof id === 'string' ? [id] : [];
|
|
87
|
+
}));
|
|
88
|
+
const added = [];
|
|
89
|
+
const alreadyListed = [];
|
|
90
|
+
for (const product of products) {
|
|
91
|
+
if (listed.has(product.id)) {
|
|
92
|
+
alreadyListed.push(product.id);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
list.push(consumable(product));
|
|
96
|
+
listed.add(product.id);
|
|
97
|
+
added.push(product.id);
|
|
98
|
+
}
|
|
99
|
+
catalogue.products = list;
|
|
100
|
+
if (created || added.length > 0) {
|
|
101
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
102
|
+
fs.writeFileSync(filePath, `${JSON.stringify(catalogue, null, 2)}\n`);
|
|
103
|
+
}
|
|
104
|
+
return { created, added, alreadyListed };
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=storekit-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storekit-config.js","sourceRoot":"","sources":["../../src/ios/storekit-config.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type { IosAppConfig } from './app-config.js';
|
|
2
|
+
import { type NativeOverrides } from './native-overrides.js';
|
|
2
3
|
export declare const TARGET_NAME = "Game";
|
|
3
4
|
export declare const PAYLOAD_DIR_NAME = "GamePayload";
|
|
4
5
|
export declare const URL_SCHEME = "bmapp";
|
|
5
6
|
/** The Info.plist orientation array for the project's declaration (§6). */
|
|
6
7
|
export declare function orientationValues(orientation: 'portrait' | 'landscape' | undefined): string[];
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* The hook creator Swift (ios/Sources/) plugs into, and the list of adopters this build found.
|
|
10
|
+
* Generated because Swift has no static registration: the CLI scans the creator's files for
|
|
11
|
+
* classes adopting the protocol (native-overrides.ts) and lists them here. Each needs an init
|
|
12
|
+
* that takes no arguments; one that lacks it fails the build with Swift's own error, naming it.
|
|
10
13
|
*/
|
|
11
|
-
export declare function
|
|
14
|
+
export declare function nativeExtensionsSwift(classNames: readonly string[]): string;
|
|
15
|
+
/**
|
|
16
|
+
* Write the whole project (everything except the payload contents, the icon PNG and the
|
|
17
|
+
* creator's own files, which have their own producers). Existing files are overwritten — the
|
|
18
|
+
* project is CLI-owned. `overrides` describes what `copyNativeOverrides` puts in it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function stampXcodeProject(projectDir: string, config: IosAppConfig, overrides?: NativeOverrides): void;
|