├── .github
├── FUNDING.yml
└── workflows
│ ├── bob.yml
│ └── trigger-site-rebuild.yml
├── .gitignore
├── LICENSE
├── README.md
├── docs
├── alpha_testers.png
├── android_purchase.png
├── google_play_products.png
├── index.md
├── ios_confirm_purchase.png
├── ios_purchase_done.png
├── itunes_connect_google_play.png
└── itunes_products.png
├── extension-iap
├── api
│ └── iap.script_api
├── ext.manifest
├── lib
│ ├── android
│ │ └── in-app-purchasing-2.0.76.jar
│ └── web
│ │ └── library_facebook_iap.js
├── manifests
│ └── android
│ │ ├── AndroidManifest.xml
│ │ ├── build.gradle
│ │ └── extension-iap.pro
└── src
│ ├── iap.h
│ ├── iap_android.cpp
│ ├── iap_emscripten.cpp
│ ├── iap_ios.mm
│ ├── iap_null.cpp
│ ├── iap_private.cpp
│ ├── iap_private.h
│ └── java
│ └── com
│ └── defold
│ └── iap
│ ├── IListProductsListener.java
│ ├── IPurchaseListener.java
│ ├── IapAmazon.java
│ ├── IapGooglePlay.java
│ └── IapJNI.java
├── game.project
├── input
└── game.input_binding
└── main
├── main.collection
├── main.gui
└── main.gui_script
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: defold
2 | patreon: Defold
3 | custom: ['https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=NBNBHTUW4GS4C']
4 |
--------------------------------------------------------------------------------
/.github/workflows/bob.yml:
--------------------------------------------------------------------------------
1 | name: Build with bob
2 |
3 | on:
4 | push:
5 | pull_request_target:
6 | schedule:
7 | # nightly at 05:00 on the 1st and 15th
8 | - cron: 0 5 1,15 * *
9 |
10 | env:
11 | VERSION_FILENAME: 'info.json'
12 | BUILD_SERVER: 'https://build.defold.com'
13 |
14 | jobs:
15 | build_with_bob:
16 | strategy:
17 | matrix:
18 | platform: [armv7-android, x86_64-linux, x86_64-win32, x86-win32, js-web]
19 | runs-on: ubuntu-latest
20 |
21 | name: Build
22 | steps:
23 | - uses: actions/checkout@v2
24 | - uses: actions/setup-java@v1
25 | with:
26 | java-version: '11.0.2'
27 |
28 | - name: Get Defold version
29 | run: |
30 | TMPVAR=`curl -s http://d.defold.com/stable/${{env.VERSION_FILENAME}} | jq -r '.sha1'`
31 | echo "DEFOLD_VERSION=${TMPVAR}" >> $GITHUB_ENV
32 | echo "Found version ${TMPVAR}"
33 |
34 | - name: Download bob.jar
35 | run: |
36 | wget -q http://d.defold.com/archive/stable/${{env.DEFOLD_VERSION}}/bob/bob.jar
37 | java -jar bob.jar --version
38 |
39 | - name: Resolve libraries
40 | run: java -jar bob.jar resolve --email a@b.com --auth 123456
41 | - name: Build
42 | run: java -jar bob.jar --platform=${{ matrix.platform }} build --archive --build-server=${{env.BUILD_SERVER}}
43 | - name: Bundle
44 | run: java -jar bob.jar --platform=${{ matrix.platform }} bundle
45 |
46 | # macOS is not technically needed for building, but we want to test bundling as well, since we're also testing the manifest merging
47 | build_with_bob_macos:
48 | strategy:
49 | matrix:
50 | platform: [armv7-darwin, x86_64-darwin]
51 | runs-on: macOS-latest
52 |
53 | name: Build
54 | steps:
55 | - uses: actions/checkout@v2
56 | - uses: actions/setup-java@v1
57 | with:
58 | java-version: '11.0.2'
59 |
60 | - name: Get Defold version
61 | run: |
62 | TMPVAR=`curl -s http://d.defold.com/stable/${{env.VERSION_FILENAME}} | jq -r '.sha1'`
63 | echo "DEFOLD_VERSION=${TMPVAR}" >> $GITHUB_ENV
64 | echo "Found version ${TMPVAR}"
65 |
66 | - name: Download bob.jar
67 | run: |
68 | wget -q http://d.defold.com/archive/stable/${{env.DEFOLD_VERSION}}/bob/bob.jar
69 | java -jar bob.jar --version
70 |
71 | - name: Resolve libraries
72 | run: java -jar bob.jar resolve --email a@b.com --auth 123456
73 | - name: Build
74 | run: java -jar bob.jar --platform=${{ matrix.platform }} build --archive --build-server=${{env.BUILD_SERVER}}
75 | - name: Bundle
76 | run: java -jar bob.jar --platform=${{ matrix.platform }} bundle
77 |
--------------------------------------------------------------------------------
/.github/workflows/trigger-site-rebuild.yml:
--------------------------------------------------------------------------------
1 | name: Trigger site rebuild
2 |
3 | on: [push]
4 |
5 | jobs:
6 | site-rebuild:
7 | runs-on: ubuntu-latest
8 |
9 | steps: [
10 | {
11 | name: 'Repository dispatch',
12 | uses: defold/repository-dispatch@1.2.1,
13 | with: {
14 | repo: 'defold/defold.github.io',
15 | token: '${{ secrets.SERVICES_GITHUB_TOKEN }}',
16 | user: 'services@defold.se',
17 | action: 'extension-iap'
18 | }
19 | }]
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.internal
2 | /build
3 | .externalToolBuilders
4 | .DS_Store
5 | Thumbs.db
6 | .lock-wscript
7 | *.pyc
8 | .project
9 | .cproject
10 | builtins
11 | _site
12 | manifest.private.der
13 | manifest.public.der
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Defold
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/defold/extension-iap/actions)
2 |
3 | # In-app purchase extension for Defold
4 |
5 | Defold [native extension](https://www.defold.com/manuals/extensions/) which provides access to In-app purchase functionality on iOS, Android (Google Play and Amazon) and Facebook Canvas platforms.
6 |
7 | [Manual, API and setup instructions](https://www.defold.com/extension-iap/) is available on the official Defold site.
8 |
--------------------------------------------------------------------------------
/docs/alpha_testers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defold/extension-iap/88028fd29f7d53cfebef752a7944814eddc44142/docs/alpha_testers.png
--------------------------------------------------------------------------------
/docs/android_purchase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defold/extension-iap/88028fd29f7d53cfebef752a7944814eddc44142/docs/android_purchase.png
--------------------------------------------------------------------------------
/docs/google_play_products.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defold/extension-iap/88028fd29f7d53cfebef752a7944814eddc44142/docs/google_play_products.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Defold In-app purchase extension API documentation
3 | brief: This manual covers how to setup and use In-App Purchases in Defold.
4 | ---
5 |
6 | # Defold In-app purchase extension API documentation
7 |
8 | This extension provides a unified, simple to use interface to several different stores for in-app purchase:
9 |
10 | * Apple’s iOS Appstore - StoreKit
11 | * Google Play Billing 5.0
12 | * Amazon 'in-app billing' 2.0.61
13 | * Facebook Canvas 'game payments'
14 |
15 | These services gives you the opportunity to sell products as:
16 |
17 | * Standard in-app products (one time billing) of consumables or non-consumables and
18 | * Subscriptions (recurring, automated billing)
19 |
20 | ::: important
21 | The current Defold interface allows full interaction with Apple's Storekit functionality. For Google Play and Facebook Canvas, the interface is identical, meaning that you can run the same code on either platform. However, some process flow might differ from platform to platform. Also note that there is currently no support for OS X purchases through the Mac Appstore.
22 | :::
23 |
24 | Detailed documentation from Apple, Google, Amazon and Facebook can be found here:
25 |
26 | * [In-App Purchase Programming Guide](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction.html).
27 | * [Google Play In-app Billing documentation](http://developer.android.com/google/play/billing/index.html).
28 | * [Amazon In-app Purchase documentation](https://developer.amazon.com/public/apis/earn/in-app-purchasing).
29 | * [Facebook game payments documentation](https://developers.facebook.com/docs/payments).
30 |
31 | ## Installation
32 | To use this library in your Defold project, add the following URL to your `game.project` dependencies:
33 |
34 | [https://github.com/defold/extension-iap/archive/master.zip](https://github.com/defold/extension-iap/archive/master.zip)
35 |
36 | We recommend using a link to a zip file of a [specific release](https://github.com/defold/extension-iap/releases).
37 |
38 | For Facebook Canvas you also need to add the [Facebook extension as a dependency](https://github.com/defold/extension-facebook).
39 |
40 |
41 | ## Testing Google Play Billing with static responses
42 |
43 | On Android it is recommended that you start implementing IAP in your app by using static responses from Google Play. This enables you to verify that everything in your app works correctly before you publish the app. Four reserved product IDs exist for testing static In-app Billing responses:
44 |
45 | `android.test.purchased`
46 | : Google Play responds as though you successfully purchased an item. The response includes a JSON string, which contains fake purchase information (for example, a fake order ID).
47 |
48 | `android.test.canceled`
49 | : Google Play responds as though the purchase was canceled. This can occur when an error is encountered in the order process, such as an invalid credit card, or when you cancel a user's order before it is charged.
50 |
51 | `android.test.refunded`
52 | : Google Play responds as though the purchase was refunded.
53 |
54 | `android.test.item_unavailable`
55 | : Google Play responds as though the item being purchased was not listed in your application's product list.
56 |
57 |
58 | ## Setting up your app for purchases/billing
59 |
60 | The procedure on iOS and Android is similar:
61 |
62 | 1. Make sure you are a registered Apple or Google Play developer.
63 | 2. Set up your project so it works on your target device. See the [iOS development](/manuals/ios) and [Android development](/manuals/android) guides.
64 | 3. Set up the app for testing:
65 |
66 | - For Android, this is done on the [Google Play Developer Console](https://play.google.com/apps/publish/).
67 | - For iOS, this is done on [iTunes Connect](https://itunesconnect.apple.com/). Make sure that your App ID (created in the "Member Center" on https://developer.apple.com) has "In-App Purchase" enabled.
68 |
69 | 
70 |
71 | 4. For Google Play, you need to _upload and publish_ an alpha *.apk* file. For iTunes Connect, you should _not upload_ the development binary to iTunes Connect until the application is ready for App Review approval. If you upload a binary to iTunes Connect and it is not fully functional, Apple will likely reject it.
72 |
73 | 5. Create products for your app.
74 |
75 | 
76 |
77 | 
78 |
79 | 6. Add test users.
80 | - The iTunes Connect page *Users and Roles* allow you to add users that can do test purchases in the _sandbox environment_. You should sign your app with a Developer certificate and use the sandbox account in Appstore on the test device.
81 | - From the Google Play Developer Console, choose *Settings > Account Details* where you can add user emails to the License Testing section. Separate the emails by commas. This allows your testers to use test purchases that don’t actually cost real money.
82 | - On Google Play, you also need to set up a Google Group for your testers. Google uses Groups to manage testers that can download your app from the Alpha and Beta stores. Click on the *Alpha Testing* tab and then *Manage list of testers* to add your Google Group as Alpha testers. The app must have passed through alpha publishing before you can see the opt-in link.
83 |
84 | 
85 |
86 | The procedure on Facebook:
87 |
88 | 1. Make sure you are a registered Facebook developer. Go to [Facebook for developers](https://developers.facebook.com/), "My Apps" and "Register as a developer", follow the steps.
89 | 2. Facebook has extensive payment functionality and requires support of both synchronous and asynchronous payments. More info here [Payment overview](https://developers.facebook.com/docs/payments/overview)
90 | 3. Set up app hosting and callback server:
91 | * You will need to set up a secure canvas URL hosting your project. How this works is explained here [Games on Facebook](https://developers.facebook.com/docs/games/gamesonfacebook/hosting).
92 | * The next step is to set up your callback server. Follow the steps here [Setting up your callback server](https://developers.facebook.com/docs/payments/realtimeupdates#yourcallbackserver).
93 | 4. Set up you canvas app. Follow the steps on [Facebook Developer Dashboard](https://developers.facebook.com/quickstarts/?platform=canvas).
94 | 5. Add test users. This is done in the "Canvas Payments" section of the app dashboard.
95 | 6. Create products for your app [Defining products](https://developers.facebook.com/docs/payments/implementation-guide/defining-products/).
96 |
97 |
98 | ## Asynchronous transactions
99 |
100 | The IAP API is asynchronous, meaning that after each request that your program sends to the server, the program will not halt and wait for a response. Instead, the program continues as ordinary and when the response arrives, a _callback_ function is invoked where you can react to the response data.
101 |
102 | To fetch all product information available:
103 |
104 | ```lua
105 | local COINS_ID = "com.defold.examples.coins"
106 | local LOGO_ID = "com.defold.examples.logo"
107 |
108 | local function product_list(self, products, error)
109 | if error == nil then
110 | for i,p in pairs(products) do
111 | print(p.ident)
112 | print(p.title)
113 | print(p.description)
114 | print(p.currency_code)
115 | print(p.price_string)
116 | end
117 | else
118 | print(error.error)
119 | end
120 | end
121 |
122 | function init(self)
123 | -- Initiate a fetch of products (max 20 at a time for Google Play)
124 | iap.list({ COINS_ID, LOGO_ID }, product_list)
125 | end
126 | ```
127 |
128 | To perform actual transactions, first register a function that will listen to transaction results, then call the store function at the appropriate time:
129 |
130 | ```lua
131 | local function iap_listener(self, transaction, error)
132 | if error == nil then
133 | if transaction.state == iap.TRANS_STATE_PURCHASING then
134 | print("Purchasing...")
135 | elseif transaction.state == iap.TRANS_STATE_PURCHASED then
136 | print("Purchased!")
137 | elseif transaction.state == iap.TRANS_STATE_UNVERIFIED then
138 | print("Unverified!")
139 | elseif transaction.state == iap.TRANS_STATE_FAILED then
140 | print("Failed!")
141 | elseif transaction.state == iap.TRANS_STATE_RESTORED then
142 | print("Restored")
143 | end
144 | else
145 | print(error.error)
146 | end
147 | end
148 |
149 | function on_message(self, message_id, message, sender)
150 | ...
151 | -- Register the function that will listen to IAP transactions.
152 | iap.set_listener(iap_listener)
153 | -- Initiate a purchase of a coin...
154 | iap.buy(COINS_ID)
155 | ...
156 | end
157 | ```
158 |
159 | The device operating system will automatically show a pop-up window allowing the user to go through with the purchase. The interface clearly indicates when you are running in the test/sandbox environment.
160 |
161 | 
162 |
163 | 
164 |
165 | 
166 |
167 |
168 | ## Synchronous payments
169 |
170 | Most payment providers only supports synchronous payments. This means that the client (your application) will receive a notification when the payment is complete, TRANS_STATE_PURCHASED. This is the final state of the payment, meaning no more callbacks will be done on this transaction.
171 |
172 |
173 | ## Asynchronous payments
174 |
175 | Some payment providers require supporting asynchronous payments. This means that the client (your application) will only receive a notification when the payment is initiated. In order to verify completion of payment, further communication needs to be done between the developer server (or client) and the payment provider in order to verify.
176 |
177 | In the case of an initiated asynchronous payment the IAP listener will receive the state TRANS_STATE_UNVERIFIED to indicate this (as opposed to TRANS_STATE_PURCHASED). This is the final state of the payment, meaning no more callbacks will be done on this transaction.
178 |
179 |
180 | ## Purchase fulfilment
181 |
182 | In order to complete a purchase from a payment provider, the application needs to signal a purchase fulfilment to the provider telling the provider the purchase has gone through (for example by developer server-side verification).
183 |
184 | IAP supports auto-completion, where fulfilment is automatically signalled to the provider when a purchase is complete (this is the default behavior). You can also disable auto-completion in the game project settings. You are then required to call `iap.finish()` when the transaction is complete, which will signal purchase fulfilment to the provider.
185 |
186 |
187 | ### Consumable vs non-consumable products
188 |
189 | The Google Play store does only support consumable products. If you need non-consumable products it is recommended to use manual fulfilment of purchases and never finish purchases for products that should be non-consumable. As long as a purchase isn't finished it will be returned as an active purchase when `iap.set_listener()` is called. If you do not call `iap.finish()` on a purchase you still need to indicate to Google Play that the purchase has been handled. You can do this by calling `iap.acknowledge()`. If you do not call `iap.acknowledge()` the purchase will be automatically refunded by Google after a few days.
190 |
191 | The Apple App Store supports non-consumable products which means that you need to finish all purchases when you provide products to your users. You can do it automatically by keeping the default behavior in the game project settings or manually (if you want to do that after server validation, for example) using `iap.finish()`.
192 |
193 |
194 | ## Transaction receipt
195 |
196 | The receipt is a signed chunk of data that can be sent to the App Store to verify that the payment was successfully processed. This is most useful when designing a store that uses a separate server to verify that payment was processed.
197 |
198 |
199 | ## Differences between supported platforms
200 |
201 | Amazon supports two different product types: subscriptions and consumable products.
202 |
203 | Google Play and Apple supports three different product types: subscriptions, consumable and non-consumable products.
204 |
205 | If you want to simulate non-consumable products on Amazon you need to make sure to not call `iap.finish()` on the product in question (and make sure to not have enabled Auto Finish Transactions in *game.project*).
206 |
207 | Calls to `iap.buy()` and `iap.set_listener()` will return all non-finished purchases on Google Play. (This will not happen on iOS)
208 |
209 | The concept of restoring purchases does not exist on Google Play/Amazon. Calls to `iap.restore()` on iOS will return all purchased products (and have product state set to TRANS_STATE_RESTORED). Calls to `iap.restore()` on Google Play will return all non-finished purchases (and have product state set to TRANS_STATE_PURCHASED).
210 |
211 |
212 | ## Troubleshooting
213 |
214 | Android `iap.list()` returns "failed to fetch product"
215 | : You need to upload and publish an *.apk* on the alpha or beta channels on the Google Play Developer Console. Also make sure that the _time and date_ on your device is correct.
216 |
217 | Android (Google Play) `iap.list()` never returns more than 20 products
218 | : Google has a [limit of 20 products per request](https://github.com/android/play-billing-samples/blob/7a94c6905a9c125518354c216b5c3094fde47ce1/TrivialDrive/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl#L62). The solution is to make multiple calls to `iap.list()` and combine the results if the number of products exceeds 20.
219 |
220 | iOS `iap.list()` returns nothing
221 | : Make sure that you’ve requested an iOS Paid Applications account, and all proper documentation has been filed. Without proper authorization, your iOS app purchasing (even test purchases) will not work.
222 |
223 | Check that the AppId you have on the "Member Center" has in-app purchases activated and that you are signing your app (or the dev-app) with a provisioning profile that is up to date with the AppId (check the "Enabled Services:" field in the provisioning profile details in the "Certificates, Identifiers & Profiles" area of "Member Center")
224 |
225 | Wait. It can take a few hours for the In-App product IDs to propagate to the Sandbox environment.
226 |
227 | iOS `iap.list()` fails logging error "Unexpected callback set"
228 | : `iap.list()` does not support nested calls. Calling `iap.list()` from an `iap.list()` callback function will be ignored, with the engine logging this error.
229 |
230 | On iOS, the "price_string" field contains '~' characters
231 | : The '~' characters are placeholders where no matching character could be found in the font file. The "price_string" field returned in the product list when using `iap.list()` is formatted with a _non breaking space_ (`\u00a0`) between the value and the currency denominator. If you render this string in the GUI, you need to add the character to the font's *extra_characters* field. On Mac OS X you can type non breaking spaces by pressing Option + SPACE. See http://en.wikipedia.org/wiki/Non-breaking_space for more information.
232 |
233 |
234 | ## Source code
235 |
236 | The source code is available on [GitHub](https://github.com/defold/extension-iap)
237 |
--------------------------------------------------------------------------------
/docs/ios_confirm_purchase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defold/extension-iap/88028fd29f7d53cfebef752a7944814eddc44142/docs/ios_confirm_purchase.png
--------------------------------------------------------------------------------
/docs/ios_purchase_done.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defold/extension-iap/88028fd29f7d53cfebef752a7944814eddc44142/docs/ios_purchase_done.png
--------------------------------------------------------------------------------
/docs/itunes_connect_google_play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defold/extension-iap/88028fd29f7d53cfebef752a7944814eddc44142/docs/itunes_connect_google_play.png
--------------------------------------------------------------------------------
/docs/itunes_products.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defold/extension-iap/88028fd29f7d53cfebef752a7944814eddc44142/docs/itunes_products.png
--------------------------------------------------------------------------------
/extension-iap/api/iap.script_api:
--------------------------------------------------------------------------------
1 | - name: iap
2 | type: table
3 | desc: Functions and constants for doing in-app purchases. Supported on iOS, Android (Google Play and Amazon)
4 | and Facebook Canvas platforms.
5 | [icon:ios] [icon:googleplay] [icon:amazon] [icon:facebook]
6 | members:
7 |
8 | #*****************************************************************************************************
9 |
10 | - name: buy
11 | type: function
12 | desc: Purchase a product.
13 | parameters:
14 | - name: id
15 | type: string
16 | desc: product to buy
17 |
18 | - name: options
19 | type: table
20 | desc: optional parameters as properties. The following parameters can be set
21 | members:
22 | - name: request_id
23 | type: string
24 | desc: Facebook only [icon:facebook]. Optional custom unique request id to
25 | set for this transaction. The id becomes attached to the payment within the Graph API.
26 | - name: token
27 | type: string
28 | desc: Google Play only [icon:googleplay]. Which subscription offer to use when buying a subscription. The token can be retrieved from
29 | the subscriptions table returned when calling iap.list()
30 |
31 | examples:
32 | - desc: |-
33 | ```lua
34 | local function iap_listener(self, transaction, error)
35 | if error == nil then
36 | -- purchase is successful.
37 | print(transaction.date)
38 | -- required if auto finish transactions is disabled in project settings
39 | if (transaction.state == iap.TRANS_STATE_PURCHASED) then
40 | -- do server-side verification of purchase here..
41 | iap.finish(transaction)
42 | end
43 | else
44 | print(error.error, error.reason)
45 | end
46 | end
47 |
48 | function init(self)
49 | iap.set_listener(iap_listener)
50 | iap.buy("my_iap")
51 | end
52 | ```
53 |
54 | #*****************************************************************************************************
55 |
56 | - name: finish
57 | type: function
58 | desc: Explicitly finish a product transaction.
59 | [icon:attention] Calling iap.finish is required on a successful transaction
60 | if `auto_finish_transactions` is disabled in project settings. Calling this function
61 | with `auto_finish_transactions` set will be ignored and a warning is printed.
62 | The `transaction.state` field must equal `iap.TRANS_STATE_PURCHASED`.
63 | parameters:
64 | - name: transaction
65 | type: table
66 | desc: transaction table parameter as supplied in listener callback
67 |
68 | #*****************************************************************************************************
69 |
70 | - name: acknowledge
71 | type: function
72 | desc: Acknowledge a transaction.
73 | [icon:attention] Calling iap.acknowledge is required on a successful transaction on Google
74 | Play unless iap.finish is called. The transaction.state field must equal iap.TRANS_STATE_PURCHASED.
75 | parameters:
76 | - name: transaction
77 | type: table
78 | desc: transaction table parameter as supplied in listener callback
79 |
80 | #*****************************************************************************************************
81 |
82 | - name: get_provider_id
83 | type: function
84 | desc: Get current iap provider
85 | returns:
86 | - name: provider_id
87 | type: constant
88 | desc: one of the following values
89 |
90 | - `iap.PROVIDER_ID_GOOGLE`
91 |
92 | - `iap.PROVIDER_ID_AMAZON`
93 |
94 | - `iap.PROVIDER_ID_APPLE`
95 |
96 | - `iap.PROVIDER_ID_FACEBOOK`
97 |
98 | #*****************************************************************************************************
99 |
100 | - name: list
101 | type: function
102 | desc: Get a list of all avaliable iap products.
103 | parameters:
104 | - name: ids
105 | type: table
106 | desc: table (array) of identifiers to get products from
107 |
108 | - name: callback
109 | type: function
110 | desc: result callback taking the following parameters
111 | parameters:
112 | - name: self
113 | type: object
114 | desc: The current object.
115 |
116 | - name: products
117 | type: table
118 | desc: a table describing the available iap products.
119 | members:
120 | - name: ident
121 | type: string
122 | desc: The product identifier.
123 |
124 | - name: title
125 | type: string
126 | desc: The product title.
127 |
128 | - name: description
129 | type: string
130 | desc: The product description.
131 |
132 | - name: price
133 | type: number
134 | desc: The price of the product.
135 | For Google Play [icon:googleplay] this is used only for in-app products
136 |
137 | - name: price_string
138 | type: string
139 | desc: The price of the product, as a formatted string (amount and currency symbol).
140 | For Google Play [icon:googleplay] this is used only for in-app products
141 |
142 | - name: currency_code
143 | type: string
144 | desc: The currency code.
145 | For Google Play [icon:googleplay] this is the merchant's locale, instead of the user's.
146 | For Google Play [icon:googleplay] this is used only for in-app products
147 |
148 | - name: subscriptions
149 | type: table
150 | desc: Only available for Google Play [icon:googleplay]. List of subscription offers.
151 | Each offer contains a token and a list of price and billing options.
152 | See https://developer.android.com/reference/com/android/billingclient/api/ProductDetails.PricingPhase
153 | members:
154 | - name: token
155 | type: string
156 | desc: The token associated with the pricing phases for the subscription.
157 |
158 | - name: pricing
159 | type: table
160 | desc: The pricing phases for the subscription.
161 | members:
162 | - name: price_string
163 | type: string
164 | desc: Formatted price for the payment cycle, including currency sign.
165 |
166 | - name: price
167 | type: number
168 | desc: Price of the payment cycle in micro-units.
169 |
170 | - name: currency_code
171 | type: string
172 | desc: ISO 4217 currency code
173 |
174 | - name: billing_period
175 | type: string
176 | desc: Billing period of the payment cycle, specified in ISO 8601 format
177 |
178 | - name: billing_cycle_count
179 | type: number
180 | desc: Number of cycles for which the billing period is applied.
181 |
182 | - name: recurrence_mode
183 | type: string
184 | desc: FINITE, INFINITE or NONE
185 |
186 | - name: error
187 | type: table
188 | desc: a table containing error information. `nil` if there is no error. - `error` (the error message)
189 |
190 | examples:
191 | - desc: |-
192 | ```lua
193 | local function iap_callback(self, products, error)
194 | if error == nil then
195 | for k,p in pairs(products) do
196 | -- present the product
197 | print(p.title)
198 | print(p.description)
199 | end
200 | else
201 | print(error.error)
202 | end
203 | end
204 |
205 | function init(self)
206 | iap.list({"my_iap"}, iap_callback)
207 | end
208 | ```
209 |
210 | #*****************************************************************************************************
211 |
212 | - name: restore
213 | type: function
214 | desc: Restore previously purchased products.
215 | returns:
216 | - name: success
217 | type: boolean
218 | desc: value is `true` if current store supports handling
219 | restored transactions, otherwise `false`.
220 |
221 | #*****************************************************************************************************
222 |
223 | - name: set_listener
224 | type: function
225 | desc: Set the callback function to receive purchase transaction events.
226 | parameters:
227 | - name: listener
228 | type: function
229 | desc: listener callback function. Pass an empty function if you no longer wish to receive callbacks.
230 | parameters:
231 | - name: self
232 | type: object
233 | desc: The current object.
234 |
235 | - name: transaction
236 | type: table
237 | desc: a table describing the transaction.
238 | members:
239 | - name: ident
240 | type: string
241 | desc: The product identifier.
242 |
243 | - name: state
244 | type: string
245 | desc: The transaction state. One of the following
246 |
247 | - `iap.TRANS_STATE_FAILED`
248 |
249 | - `iap.TRANS_STATE_PURCHASED`
250 |
251 | - `iap.TRANS_STATE_PURCHASING`
252 |
253 | - `iap.TRANS_STATE_RESTORED`
254 |
255 | - `iap.TRANS_STATE_UNVERIFIED`
256 |
257 | - name: date
258 | type: string
259 | desc: The date and time for the transaction.
260 |
261 | - name: trans_ident
262 | type: string
263 | desc: The transaction identifier. This field is only set when `state` is
264 | `TRANS_STATE_RESTORED`, `TRANS_STATE_UNVERIFIED` or `TRANS_STATE_PURCHASED`.
265 |
266 | - name: receipt
267 | type: string
268 | desc: The transaction receipt. This field is only set when `state` is `TRANS_STATE_PURCHASED` or `TRANS_STATE_UNVERIFIED`.
269 |
270 | - name: original_trans
271 | type: string
272 | desc: Apple only [icon:apple]. The original transaction. This field is only set when `state` is `TRANS_STATE_RESTORED`.
273 |
274 | - name: original_json
275 | type: string
276 | desc: Android only [icon:android]. The purchase order details in JSON format.
277 |
278 | - name: signature
279 | type: string
280 | desc: Google Play only [icon:googleplay]. A string containing the signature of the purchase data that was signed with the private key of the developer.
281 |
282 | - name: request_id
283 | type: string
284 | desc: Facebook only [icon:facebook]. This field is set to the optional custom unique request id `request_id` if set in the `iap.buy()` call parameters.
285 |
286 | - name: user_id
287 | type: string
288 | desc: Amazon Pay only [icon:amazon]. The user ID.
289 |
290 | - name: is_sandbox_mode
291 | type: boolean
292 | desc: Amazon Pay only [icon:amazon]. If `true`, the SDK is running in Sandbox mode.
293 | This only allows interactions with the Amazon AppTester. Use this mode only for testing locally.
294 |
295 | - name: cancel_date
296 | type: string
297 | desc: Amazon Pay only [icon:amazon]. The cancel date for the purchase. This field is only set if the purchase is canceled.
298 |
299 | - name: canceled
300 | type: string
301 | desc: Amazon Pay only [icon:amazon]. Is set to `true` if the receipt was canceled or has expired; otherwise `false`.
302 |
303 | - name: error
304 | type: table
305 | desc: a table containing error information. `nil` if there is no error. `error` - the error message.
306 | `reason` - the reason for the error, value can be one of the following constants
307 |
308 | - `iap.REASON_UNSPECIFIED`
309 |
310 | - `iap.REASON_USER_CANCELED`
311 |
312 | #*****************************************************************************************************
313 |
314 | - name: PROVIDER_ID_AMAZON
315 | type: number
316 | desc: provider id for Amazon
317 |
318 | - name: PROVIDER_ID_APPLE
319 | type: number
320 | desc: provider id for Apple
321 |
322 | - name: PROVIDER_ID_FACEBOOK
323 | type: number
324 | desc: provider id for Facebook
325 |
326 | - name: PROVIDER_ID_GOOGLE
327 | type: number
328 | desc: iap provider id for Google
329 |
330 | - name: REASON_UNSPECIFIED
331 | type: number
332 | desc: unspecified error reason
333 |
334 | - name: REASON_USER_CANCELED
335 | type: number
336 | desc: user canceled reason
337 |
338 | - name: TRANS_STATE_FAILED
339 | type: number
340 | desc: transaction failed state
341 |
342 | - name: TRANS_STATE_PURCHASED
343 | type: number
344 | desc: transaction purchased state
345 |
346 | - name: TRANS_STATE_PURCHASING
347 | type: number
348 | desc: transaction purchasing state
349 | This is an intermediate mode followed by TRANS_STATE_PURCHASED. Store provider support dependent.
350 |
351 | - name: TRANS_STATE_RESTORED
352 | type: number
353 | desc: transaction restored state
354 | This is only available on store providers supporting restoring purchases.
355 |
356 | - name: TRANS_STATE_UNVERIFIED
357 | type: number
358 | desc: transaction unverified state, requires verification of purchase
359 |
--------------------------------------------------------------------------------
/extension-iap/ext.manifest:
--------------------------------------------------------------------------------
1 | name: IAPExt
2 |
3 | platforms:
4 | arm64-ios:
5 | context:
6 | weakFrameworks: ['StoreKit', 'UIKit', 'Foundation']
7 |
8 | x86_64-ios:
9 | context:
10 | weakFrameworks: ['StoreKit', 'UIKit', 'Foundation']
11 |
--------------------------------------------------------------------------------
/extension-iap/lib/android/in-app-purchasing-2.0.76.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defold/extension-iap/88028fd29f7d53cfebef752a7944814eddc44142/extension-iap/lib/android/in-app-purchasing-2.0.76.jar
--------------------------------------------------------------------------------
/extension-iap/lib/web/library_facebook_iap.js:
--------------------------------------------------------------------------------
1 |
2 | var LibraryFacebookIAP = {
3 | $FBinner: {
4 | // NOTE: Also defined in iap.h
5 | TransactionState :
6 | {
7 | TRANS_STATE_PURCHASING : 0,
8 | TRANS_STATE_PURCHASED : 1,
9 | TRANS_STATE_FAILED : 2,
10 | TRANS_STATE_RESTORED : 3,
11 | TRANS_STATE_UNVERIFIED : 4
12 | },
13 |
14 | // NOTE: Also defined in iap.h
15 | BillingResponse :
16 | {
17 | BILLING_RESPONSE_RESULT_OK : 0,
18 | BILLING_RESPONSE_RESULT_USER_CANCELED : 1,
19 | BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE : 3,
20 | BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE : 4,
21 | BILLING_RESPONSE_RESULT_DEVELOPER_ERROR : 5,
22 | BILLING_RESPONSE_RESULT_ERROR : 6,
23 | BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED : 7,
24 | BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED : 8
25 | },
26 |
27 | // See Facebook definitions https://developers.facebook.com/docs/payments/reference/errorcodes
28 | FBPaymentResponse :
29 | {
30 | FB_PAYMENT_RESPONSE_USERCANCELED : 1383010,
31 | FB_PAYMENT_RESPONSE_APPINVALIDITEMPARAM : 1383051
32 | },
33 |
34 | http_callback: function(xmlhttp, callback, lua_callback, products, product_ids, product_count, url_index, url_count) {
35 | if (xmlhttp.readyState == 4) {
36 | if(xmlhttp.status == 200) {
37 | var xmlDoc = document.createElement( 'html' );
38 | xmlDoc.innerHTML = xmlhttp.responseText;
39 | var elements = xmlDoc.getElementsByTagName('meta');
40 |
41 | var productInfo = {};
42 | for (var i=0; i
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/extension-iap/manifests/android/build.gradle:
--------------------------------------------------------------------------------
1 | repositories {
2 | mavenCentral()
3 | }
4 |
5 | dependencies {
6 | implementation 'com.android.billingclient:billing:7.0.0'
7 | }
8 |
--------------------------------------------------------------------------------
/extension-iap/manifests/android/extension-iap.pro:
--------------------------------------------------------------------------------
1 | -keep class com.defold.iap.** {
2 | public ;
3 | }
4 |
5 |
--------------------------------------------------------------------------------
/extension-iap/src/iap.h:
--------------------------------------------------------------------------------
1 | #if defined(DM_PLATFORM_HTML5) || defined(DM_PLATFORM_ANDROID) || defined(DM_PLATFORM_IOS)
2 |
3 | #ifndef DM_IAP_EXTENSION
4 | #define DM_IAP_EXTENSION
5 |
6 | // NOTE: Also defined in library_facebook_iap.js
7 | // NOTE: Also defined in IapJNI.java
8 |
9 | enum TransactionState
10 | {
11 | TRANS_STATE_PURCHASING = 0,
12 | TRANS_STATE_PURCHASED = 1,
13 | TRANS_STATE_FAILED = 2,
14 | TRANS_STATE_RESTORED = 3,
15 | TRANS_STATE_UNVERIFIED = 4,
16 | };
17 |
18 | enum ErrorReason
19 | {
20 | REASON_UNSPECIFIED = 0,
21 | REASON_USER_CANCELED = 1,
22 | };
23 |
24 | enum BillingResponse
25 | {
26 | BILLING_RESPONSE_RESULT_OK = 0,
27 | BILLING_RESPONSE_RESULT_USER_CANCELED = 1,
28 | BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3,
29 | BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4,
30 | BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5,
31 | BILLING_RESPONSE_RESULT_ERROR = 6,
32 | BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7,
33 | BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8,
34 | BILLING_RESPONSE_RESULT_NETWORK_ERROR = 9,
35 | };
36 |
37 | enum ProviderId
38 | {
39 | PROVIDER_ID_GOOGLE = 0,
40 | PROVIDER_ID_AMAZON = 1,
41 | PROVIDER_ID_APPLE = 2,
42 | PROVIDER_ID_FACEBOOK = 3,
43 | };
44 |
45 | #endif // DM_IAP_EXTENSION
46 |
47 | #endif // DM_PLATFORM_HTML5 || DM_PLATFORM_ANDROID || DM_PLATFORM_IOS
48 |
--------------------------------------------------------------------------------
/extension-iap/src/iap_android.cpp:
--------------------------------------------------------------------------------
1 | #if defined(DM_PLATFORM_ANDROID)
2 |
3 | #include
4 | #include
5 |
6 | #include
7 | #include
8 | #include "iap.h"
9 | #include "iap_private.h"
10 |
11 | #define LIB_NAME "iap"
12 |
13 | struct IAP
14 | {
15 | IAP()
16 | {
17 | memset(this, 0, sizeof(*this));
18 | m_autoFinishTransactions = true;
19 | m_ProviderId = PROVIDER_ID_GOOGLE;
20 | }
21 | bool m_autoFinishTransactions;
22 | int m_ProviderId;
23 |
24 | dmScript::LuaCallbackInfo* m_Listener;
25 |
26 | jobject m_IAP;
27 | jobject m_IAPJNI;
28 | jmethodID m_List;
29 | jmethodID m_Stop;
30 | jmethodID m_Buy;
31 | jmethodID m_Restore;
32 | jmethodID m_ProcessPendingConsumables;
33 | jmethodID m_AcknowledgeTransaction;
34 | jmethodID m_FinishTransaction;
35 |
36 | IAPCommandQueue m_CommandQueue;
37 | };
38 |
39 | static IAP g_IAP;
40 |
41 | static int IAP_ProcessPendingTransactions(lua_State* L)
42 | {
43 | DM_LUA_STACK_CHECK(L, 0);
44 |
45 | dmAndroid::ThreadAttacher threadAttacher;
46 | JNIEnv* env = threadAttacher.GetEnv();
47 | env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_ProcessPendingConsumables, g_IAP.m_IAPJNI);
48 |
49 | return 0;
50 | }
51 |
52 | static int IAP_List(lua_State* L)
53 | {
54 | DM_LUA_STACK_CHECK(L, 0);
55 |
56 | char* buf = IAP_List_CreateBuffer(L);
57 | if( buf == 0 )
58 | {
59 | return 0;
60 | }
61 |
62 | dmAndroid::ThreadAttacher threadAttacher;
63 | JNIEnv* env = threadAttacher.GetEnv();
64 | IAPCommand* cmd = new IAPCommand;
65 | cmd->m_Callback = dmScript::CreateCallback(L, 2);
66 | cmd->m_Command = IAP_PRODUCT_RESULT;
67 |
68 | jstring products = env->NewStringUTF(buf);
69 | env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_List, products, g_IAP.m_IAPJNI, (jlong)cmd);
70 | env->DeleteLocalRef(products);
71 |
72 | free(buf);
73 | return 0;
74 | }
75 |
76 | static int IAP_Buy(lua_State* L)
77 | {
78 | DM_LUA_STACK_CHECK(L, 0);
79 |
80 | int top = lua_gettop(L);
81 | const char* id = luaL_checkstring(L, 1);
82 | const char* token = "";
83 |
84 | if (top >= 2 && lua_istable(L, 2)) {
85 | luaL_checktype(L, 2, LUA_TTABLE);
86 | lua_pushvalue(L, 2);
87 | lua_getfield(L, -1, "token");
88 | token = lua_isnil(L, -1) ? "" : luaL_checkstring(L, -1);
89 | lua_pop(L, 2);
90 | }
91 |
92 | dmAndroid::ThreadAttacher threadAttacher;
93 | JNIEnv* env = threadAttacher.GetEnv();
94 | jstring ids = env->NewStringUTF(id);
95 | jstring tokens = env->NewStringUTF(token);
96 | env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Buy, ids, tokens, g_IAP.m_IAPJNI);
97 | env->DeleteLocalRef(ids);
98 | env->DeleteLocalRef(tokens);
99 |
100 | return 0;
101 | }
102 |
103 | static int IAP_Finish(lua_State* L)
104 | {
105 | DM_LUA_STACK_CHECK(L, 0);
106 |
107 | if(g_IAP.m_autoFinishTransactions)
108 | {
109 | dmLogWarning("Calling iap.finish when autofinish transactions is enabled. Ignored.");
110 | return 0;
111 | }
112 |
113 | luaL_checktype(L, 1, LUA_TTABLE);
114 |
115 | lua_getfield(L, -1, "state");
116 | if (lua_isnumber(L, -1))
117 | {
118 | if(lua_tointeger(L, -1) != TRANS_STATE_PURCHASED)
119 | {
120 | dmLogError("Invalid transaction state (must be iap.TRANS_STATE_PURCHASED).");
121 | lua_pop(L, 1);
122 | return 0;
123 | }
124 | }
125 | lua_pop(L, 1);
126 |
127 | lua_getfield(L, -1, "receipt");
128 | if (!lua_isstring(L, -1)) {
129 | dmLogError("Transaction error. Invalid transaction data, does not contain 'receipt' key.");
130 | lua_pop(L, 1);
131 | }
132 | else
133 | {
134 | const char * receipt = lua_tostring(L, -1);
135 | lua_pop(L, 1);
136 |
137 | dmAndroid::ThreadAttacher threadAttacher;
138 | JNIEnv* env = threadAttacher.GetEnv();
139 | jstring receiptUTF = env->NewStringUTF(receipt);
140 | env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_FinishTransaction, receiptUTF, g_IAP.m_IAPJNI);
141 | env->DeleteLocalRef(receiptUTF);
142 | }
143 |
144 | return 0;
145 | }
146 |
147 | static int IAP_Acknowledge(lua_State* L)
148 | {
149 | DM_LUA_STACK_CHECK(L, 0);
150 |
151 | luaL_checktype(L, 1, LUA_TTABLE);
152 |
153 | lua_getfield(L, -1, "state");
154 | if (lua_isnumber(L, -1))
155 | {
156 | if(lua_tointeger(L, -1) != TRANS_STATE_PURCHASED)
157 | {
158 | dmLogError("Invalid transaction state (must be iap.TRANS_STATE_PURCHASED).");
159 | lua_pop(L, 1);
160 | return 0;
161 | }
162 | }
163 | lua_pop(L, 1);
164 |
165 | lua_getfield(L, -1, "receipt");
166 | if (!lua_isstring(L, -1)) {
167 | dmLogError("Transaction error. Invalid transaction data, does not contain 'receipt' key.");
168 | lua_pop(L, 1);
169 | }
170 | else
171 | {
172 | const char * receipt = lua_tostring(L, -1);
173 | lua_pop(L, 1);
174 |
175 | dmAndroid::ThreadAttacher threadAttacher;
176 | JNIEnv* env = threadAttacher.GetEnv();
177 | jstring receiptUTF = env->NewStringUTF(receipt);
178 | env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_AcknowledgeTransaction, receiptUTF, g_IAP.m_IAPJNI);
179 | env->DeleteLocalRef(receiptUTF);
180 | }
181 |
182 | return 0;
183 | }
184 |
185 | static int IAP_Restore(lua_State* L)
186 | {
187 | // TODO: Missing callback here for completion/error
188 | // See iap_ios.mm
189 | DM_LUA_STACK_CHECK(L, 1);
190 |
191 | dmAndroid::ThreadAttacher threadAttacher;
192 | JNIEnv* env = threadAttacher.GetEnv();
193 | env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Restore, g_IAP.m_IAPJNI);
194 |
195 | lua_pushboolean(L, 1);
196 | return 1;
197 | }
198 |
199 | static int IAP_SetListener(lua_State* L)
200 | {
201 | DM_LUA_STACK_CHECK(L, 0);
202 |
203 | IAP* iap = &g_IAP;
204 |
205 | bool had_previous = iap->m_Listener != 0;
206 |
207 | if (iap->m_Listener)
208 | dmScript::DestroyCallback(iap->m_Listener);
209 |
210 | iap->m_Listener = dmScript::CreateCallback(L, 1);
211 |
212 | // On first set listener, trigger process old ones.
213 | if (!had_previous) {
214 | dmAndroid::ThreadAttacher threadAttacher;
215 | JNIEnv* env = threadAttacher.GetEnv();
216 | env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_ProcessPendingConsumables, g_IAP.m_IAPJNI);
217 | }
218 | return 0;
219 | }
220 |
221 | static int IAP_GetProviderId(lua_State* L)
222 | {
223 | DM_LUA_STACK_CHECK(L, 1);
224 |
225 | lua_pushinteger(L, g_IAP.m_ProviderId);
226 | return 1;
227 | }
228 |
229 | static const luaL_reg IAP_methods[] =
230 | {
231 | {"list", IAP_List},
232 | {"buy", IAP_Buy},
233 | {"finish", IAP_Finish},
234 | {"acknowledge", IAP_Acknowledge},
235 | {"restore", IAP_Restore},
236 | {"set_listener", IAP_SetListener},
237 | {"get_provider_id", IAP_GetProviderId},
238 | {"process_pending_transactions", IAP_ProcessPendingTransactions},
239 | {0, 0}
240 | };
241 |
242 |
243 | #ifdef __cplusplus
244 | extern "C" {
245 | #endif
246 |
247 |
248 | JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onProductsResult(JNIEnv* env, jobject, jint responseCode, jstring productList, jlong cmdHandle)
249 | {
250 | const char* pl = 0;
251 | if (productList)
252 | {
253 | pl = env->GetStringUTFChars(productList, 0);
254 | }
255 |
256 | IAPCommand* cmd = (IAPCommand*)cmdHandle;
257 | cmd->m_ResponseCode = responseCode;
258 | if (pl)
259 | {
260 | cmd->m_Data = strdup(pl);
261 | env->ReleaseStringUTFChars(productList, pl);
262 | }
263 | IAP_Queue_Push(&g_IAP.m_CommandQueue, cmd);
264 | }
265 |
266 | JNIEXPORT void JNICALL Java_com_defold_iap_IapJNI_onPurchaseResult__ILjava_lang_String_2(JNIEnv* env, jobject, jint responseCode, jstring purchaseData)
267 | {
268 | dmLogInfo("Java_com_defold_iap_IapJNI_onPurchaseResult__ILjava_lang_String_2 %d", (int)responseCode);
269 | const char* pd = 0;
270 | if (purchaseData)
271 | {
272 | pd = env->GetStringUTFChars(purchaseData, 0);
273 | }
274 |
275 | IAPCommand cmd;
276 | cmd.m_Callback = g_IAP.m_Listener;
277 | cmd.m_Command = IAP_PURCHASE_RESULT;
278 | cmd.m_ResponseCode = responseCode;
279 | if (pd)
280 | {
281 | cmd.m_Data = strdup(pd);
282 | env->ReleaseStringUTFChars(purchaseData, pd);
283 | }
284 | IAP_Queue_Push(&g_IAP.m_CommandQueue, &cmd);
285 | }
286 |
287 | #ifdef __cplusplus
288 | }
289 | #endif
290 |
291 | static void HandleProductResult(const IAPCommand* cmd)
292 | {
293 | if (cmd->m_Callback == 0)
294 | {
295 | dmLogWarning("Received product list but no listener was set!");
296 | return;
297 | }
298 |
299 | lua_State* L = dmScript::GetCallbackLuaContext(cmd->m_Callback);
300 | int top = lua_gettop(L);
301 |
302 | if (!dmScript::SetupCallback(cmd->m_Callback))
303 | {
304 | assert(top == lua_gettop(L));
305 | return;
306 | }
307 |
308 | if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_OK) {
309 | const char* json = (const char*)cmd->m_Data;
310 | dmScript::JsonToLua(L, json, strlen(json)); // throws lua error if it fails
311 | lua_pushnil(L);
312 | } else {
313 | dmLogError("IAP error %d", cmd->m_ResponseCode);
314 | lua_pushnil(L);
315 | IAP_PushError(L, "failed to fetch product", REASON_UNSPECIFIED);
316 | }
317 |
318 | dmScript::PCall(L, 3, 0);
319 |
320 | dmScript::TeardownCallback(cmd->m_Callback);
321 | dmScript::DestroyCallback(cmd->m_Callback);
322 |
323 | assert(top == lua_gettop(L));
324 | }
325 |
326 | static void HandlePurchaseResult(const IAPCommand* cmd)
327 | {
328 | if (cmd->m_Callback == 0)
329 | {
330 | dmLogWarning("Received purchase result but no listener was set!");
331 | return;
332 | }
333 |
334 | lua_State* L = dmScript::GetCallbackLuaContext(cmd->m_Callback);
335 | int top = lua_gettop(L);
336 |
337 | if (!dmScript::SetupCallback(cmd->m_Callback))
338 | {
339 | assert(top == lua_gettop(L));
340 | return;
341 | }
342 |
343 | if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_OK) {
344 | if (cmd->m_Data != 0) {
345 | const char* json = (const char*)cmd->m_Data;
346 | dmScript::JsonToLua(L, json, strlen(json)); // throws lua error if it fails
347 | lua_pushnil(L);
348 | } else {
349 | dmLogError("IAP error, purchase response was null");
350 | lua_pushnil(L);
351 | IAP_PushError(L, "purchase response was null", REASON_UNSPECIFIED);
352 | }
353 | } else if (cmd->m_ResponseCode == BILLING_RESPONSE_RESULT_USER_CANCELED) {
354 | lua_pushnil(L);
355 | IAP_PushError(L, "user canceled purchase", REASON_USER_CANCELED);
356 | } else {
357 | dmLogError("IAP error %d", cmd->m_ResponseCode);
358 | lua_pushnil(L);
359 | IAP_PushError(L, "failed to buy product", REASON_UNSPECIFIED);
360 | }
361 |
362 | dmScript::PCall(L, 3, 0);
363 |
364 | dmScript::TeardownCallback(cmd->m_Callback);
365 |
366 | assert(top == lua_gettop(L));
367 | }
368 |
369 | static dmExtension::Result InitializeIAP(dmExtension::Params* params)
370 | {
371 | IAP_Queue_Create(&g_IAP.m_CommandQueue);
372 |
373 | g_IAP.m_autoFinishTransactions = dmConfigFile::GetInt(params->m_ConfigFile, "iap.auto_finish_transactions", 1) == 1;
374 |
375 | dmAndroid::ThreadAttacher threadAttacher;
376 | JNIEnv* env = threadAttacher.GetEnv();
377 |
378 | const char* provider = dmConfigFile::GetString(params->m_ConfigFile, "android.iap_provider", "GooglePlay");
379 | const char* class_name = "com.defold.iap.IapGooglePlay";
380 |
381 | g_IAP.m_ProviderId = PROVIDER_ID_GOOGLE;
382 | if (!strcmp(provider, "Amazon")) {
383 | g_IAP.m_ProviderId = PROVIDER_ID_AMAZON;
384 | class_name = "com.defold.iap.IapAmazon";
385 | }
386 | else if (strcmp(provider, "GooglePlay")) {
387 | dmLogWarning("Unknown IAP provider name [%s], defaulting to GooglePlay", provider);
388 | }
389 |
390 | jclass iap_class = dmAndroid::LoadClass(env, class_name);
391 | jclass iap_jni_class = dmAndroid::LoadClass(env, "com.defold.iap.IapJNI");
392 |
393 | g_IAP.m_List = env->GetMethodID(iap_class, "listItems", "(Ljava/lang/String;Lcom/defold/iap/IListProductsListener;J)V");
394 | g_IAP.m_Buy = env->GetMethodID(iap_class, "buy", "(Ljava/lang/String;Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V");
395 | g_IAP.m_Restore = env->GetMethodID(iap_class, "restore", "(Lcom/defold/iap/IPurchaseListener;)V");
396 | g_IAP.m_Stop = env->GetMethodID(iap_class, "stop", "()V");
397 | g_IAP.m_ProcessPendingConsumables = env->GetMethodID(iap_class, "processPendingConsumables", "(Lcom/defold/iap/IPurchaseListener;)V");
398 | g_IAP.m_FinishTransaction = env->GetMethodID(iap_class, "finishTransaction", "(Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V");
399 | g_IAP.m_AcknowledgeTransaction = env->GetMethodID(iap_class, "acknowledgeTransaction", "(Ljava/lang/String;Lcom/defold/iap/IPurchaseListener;)V");
400 |
401 | jmethodID jni_constructor = env->GetMethodID(iap_class, "", "(Landroid/app/Activity;Z)V");
402 | g_IAP.m_IAP = env->NewGlobalRef(env->NewObject(iap_class, jni_constructor, threadAttacher.GetActivity()->clazz, g_IAP.m_autoFinishTransactions));
403 |
404 | jni_constructor = env->GetMethodID(iap_jni_class, "", "()V");
405 | g_IAP.m_IAPJNI = env->NewGlobalRef(env->NewObject(iap_jni_class, jni_constructor));
406 |
407 | lua_State*L = params->m_L;
408 | int top = lua_gettop(L);
409 | luaL_register(L, LIB_NAME, IAP_methods);
410 |
411 | IAP_PushConstants(L);
412 |
413 | lua_pop(L, 1);
414 | assert(top == lua_gettop(L));
415 |
416 | return dmExtension::RESULT_OK;
417 | }
418 |
419 | static void IAP_OnCommand(IAPCommand* cmd, void*)
420 | {
421 | switch (cmd->m_Command)
422 | {
423 | case IAP_PRODUCT_RESULT:
424 | HandleProductResult(cmd);
425 | break;
426 | case IAP_PURCHASE_RESULT:
427 | HandlePurchaseResult(cmd);
428 | break;
429 |
430 | default:
431 | assert(false);
432 | }
433 |
434 | if (cmd->m_Data) {
435 | free(cmd->m_Data);
436 | }
437 | }
438 |
439 | static dmExtension::Result UpdateIAP(dmExtension::Params* params)
440 | {
441 | IAP_Queue_Flush(&g_IAP.m_CommandQueue, IAP_OnCommand, 0);
442 | return dmExtension::RESULT_OK;
443 | }
444 |
445 | static dmExtension::Result FinalizeIAP(dmExtension::Params* params)
446 | {
447 | IAP_Queue_Destroy(&g_IAP.m_CommandQueue);
448 |
449 | if (params->m_L == dmScript::GetCallbackLuaContext(g_IAP.m_Listener)) {
450 | dmScript::DestroyCallback(g_IAP.m_Listener);
451 | g_IAP.m_Listener = 0;
452 | }
453 |
454 | dmAndroid::ThreadAttacher threadAttacher;
455 | JNIEnv* env = threadAttacher.GetEnv();
456 | env->CallVoidMethod(g_IAP.m_IAP, g_IAP.m_Stop);
457 | env->DeleteGlobalRef(g_IAP.m_IAP);
458 | env->DeleteGlobalRef(g_IAP.m_IAPJNI);
459 | g_IAP.m_IAP = NULL;
460 | return dmExtension::RESULT_OK;
461 | }
462 |
463 | DM_DECLARE_EXTENSION(IAPExt, "IAP", 0, 0, InitializeIAP, UpdateIAP, 0, FinalizeIAP)
464 |
465 | #endif //DM_PLATFORM_ANDROID
466 |
--------------------------------------------------------------------------------
/extension-iap/src/iap_emscripten.cpp:
--------------------------------------------------------------------------------
1 | #if defined(DM_PLATFORM_HTML5)
2 |
3 | #include
4 |
5 | #include
6 | #include
7 |
8 | #include "iap.h"
9 | #include "iap_private.h"
10 |
11 | #define LIB_NAME "iap"
12 |
13 | struct IAP
14 | {
15 | IAP()
16 | {
17 | memset(this, 0, sizeof(*this));
18 | m_autoFinishTransactions = true;
19 | }
20 |
21 | dmScript::LuaCallbackInfo* m_Listener;
22 | int m_InitCount;
23 | bool m_autoFinishTransactions;
24 | } g_IAP;
25 |
26 | typedef void (*OnIAPFBList)(void* luacallback, const char* json);
27 | typedef void (*OnIAPFBListenerCallback)(void* luacallback, const char* json, int error_code);
28 |
29 | extern "C" {
30 | // Implementation in library_facebook_iap.js
31 | void dmIAPFBList(const char* item_ids, OnIAPFBList callback, dmScript::LuaCallbackInfo* luacallback);
32 | void dmIAPFBBuy(const char* item_id, const char* request_id, OnIAPFBListenerCallback callback, dmScript::LuaCallbackInfo* luacallback);
33 | }
34 |
35 | static void IAPList_Callback(void* luacallback, const char* result_json)
36 | {
37 | dmScript::LuaCallbackInfo* callback = (dmScript::LuaCallbackInfo*)luacallback;
38 | lua_State* L = dmScript::GetCallbackLuaContext(callback);
39 | DM_LUA_STACK_CHECK(L, 0);
40 |
41 | if (!dmScript::SetupCallback(callback))
42 | {
43 | dmScript::DestroyCallback(callback);
44 | return;
45 | }
46 |
47 | if(result_json != 0)
48 | {
49 | dmScript::JsonToLua(L, result_json, strlen(result_json)); // throws lua error if it fails
50 | lua_pushnil(L);
51 | }
52 | else
53 | {
54 | dmLogError("Got empty list result.");
55 | lua_pushnil(L);
56 | IAP_PushError(L, "Got empty list result.", REASON_UNSPECIFIED);
57 | }
58 |
59 | dmScript::PCall(L, 3, 0);
60 |
61 | dmScript::DestroyCallback(callback);
62 | dmScript::TeardownCallback(callback);
63 | }
64 |
65 | static int IAP_ProcessPendingTransactions(lua_State* L)
66 | {
67 | return 0;
68 | }
69 |
70 | static int IAP_List(lua_State* L)
71 | {
72 | DM_LUA_STACK_CHECK(L, 0);
73 |
74 | char* buf = IAP_List_CreateBuffer(L);
75 | if( buf == 0 )
76 | {
77 | return 0;
78 | }
79 |
80 | dmScript::LuaCallbackInfo* callback = dmScript::CreateCallback(L, 2);
81 |
82 | dmIAPFBList(buf, (OnIAPFBList)IAPList_Callback, callback);
83 |
84 | free(buf);
85 | return 0;
86 | }
87 |
88 | static void IAPListener_Callback(void* luacallback, const char* result_json, int error_code)
89 | {
90 | dmScript::LuaCallbackInfo* callback = (dmScript::LuaCallbackInfo*)luacallback;
91 | lua_State* L = dmScript::GetCallbackLuaContext(callback);
92 | DM_LUA_STACK_CHECK(L, 0);
93 |
94 | if (!dmScript::SetupCallback(callback))
95 | {
96 | return;
97 | }
98 |
99 | if (result_json) {
100 | dmScript::JsonToLua(L, result_json, strlen(result_json)); // throws lua error if it fails
101 | lua_pushnil(L);
102 | } else {
103 | lua_pushnil(L);
104 | switch(error_code)
105 | {
106 | case BILLING_RESPONSE_RESULT_USER_CANCELED:
107 | IAP_PushError(L, "user canceled purchase", REASON_USER_CANCELED);
108 | break;
109 |
110 | case BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED:
111 | IAP_PushError(L, "product already owned", REASON_UNSPECIFIED);
112 | break;
113 |
114 | default:
115 | dmLogError("IAP error %d", error_code);
116 | IAP_PushError(L, "failed to buy product", REASON_UNSPECIFIED);
117 | break;
118 | }
119 | }
120 |
121 | dmScript::PCall(L, 3, 0);
122 |
123 | dmScript::TeardownCallback(callback);
124 | }
125 |
126 |
127 | static int IAP_Buy(lua_State* L)
128 | {
129 | DM_LUA_STACK_CHECK(L, 0);
130 |
131 | if (!g_IAP.m_Listener) {
132 | dmLogError("No callback set");
133 | return 0;
134 | }
135 |
136 | int top = lua_gettop(L);
137 | const char* id = luaL_checkstring(L, 1);
138 | const char* request_id = 0x0;
139 |
140 | if (top >= 2 && lua_istable(L, 2)) {
141 | luaL_checktype(L, 2, LUA_TTABLE);
142 | lua_pushvalue(L, 2);
143 | lua_getfield(L, -1, "request_id");
144 | request_id = lua_isnil(L, -1) ? 0x0 : luaL_checkstring(L, -1);
145 | lua_pop(L, 2);
146 | }
147 |
148 | dmIAPFBBuy(id, request_id, (OnIAPFBListenerCallback)IAPListener_Callback, g_IAP.m_Listener);
149 | return 0;
150 | }
151 |
152 | static int IAP_SetListener(lua_State* L)
153 | {
154 | DM_LUA_STACK_CHECK(L, 0);
155 | if (g_IAP.m_Listener)
156 | dmScript::DestroyCallback(g_IAP.m_Listener);
157 | g_IAP.m_Listener = dmScript::CreateCallback(L, 1);
158 | return 0;
159 | }
160 |
161 | static int IAP_Finish(lua_State* L)
162 | {
163 | return 0;
164 | }
165 |
166 | static int IAP_Acknowledge(lua_State* L)
167 | {
168 | return 0;
169 | }
170 |
171 | static int IAP_Restore(lua_State* L)
172 | {
173 | DM_LUA_STACK_CHECK(L, 1);
174 | lua_pushboolean(L, 0);
175 | return 1;
176 | }
177 |
178 | static int IAP_GetProviderId(lua_State* L)
179 | {
180 | DM_LUA_STACK_CHECK(L, 1);
181 | lua_pushinteger(L, PROVIDER_ID_FACEBOOK);
182 | return 1;
183 | }
184 |
185 | static const luaL_reg IAP_methods[] =
186 | {
187 | {"list", IAP_List},
188 | {"buy", IAP_Buy},
189 | {"finish", IAP_Finish},
190 | {"acknowledge", IAP_Acknowledge},
191 | {"restore", IAP_Restore},
192 | {"set_listener", IAP_SetListener},
193 | {"get_provider_id", IAP_GetProviderId},
194 | {"process_pending_transactions", IAP_ProcessPendingTransactions},
195 | {0, 0}
196 | };
197 |
198 | static dmExtension::Result InitializeIAP(dmExtension::Params* params)
199 | {
200 | if (g_IAP.m_InitCount == 0) {
201 | g_IAP.m_autoFinishTransactions = dmConfigFile::GetInt(params->m_ConfigFile, "iap.auto_finish_transactions", 1) == 1;
202 | }
203 | g_IAP.m_InitCount++;
204 | lua_State* L = params->m_L;
205 | int top = lua_gettop(L);
206 | luaL_register(L, LIB_NAME, IAP_methods);
207 |
208 | IAP_PushConstants(L);
209 |
210 | lua_pop(L, 1);
211 | assert(top == lua_gettop(L));
212 | return dmExtension::RESULT_OK;
213 | }
214 |
215 | static dmExtension::Result FinalizeIAP(dmExtension::Params* params)
216 | {
217 | --g_IAP.m_InitCount;
218 | if (g_IAP.m_Listener && g_IAP.m_InitCount == 0)
219 | {
220 | dmScript::DestroyCallback(g_IAP.m_Listener);
221 | g_IAP.m_Listener = 0;
222 | }
223 | return dmExtension::RESULT_OK;
224 | }
225 |
226 | DM_DECLARE_EXTENSION(IAPExt, "IAP", 0, 0, InitializeIAP, 0, 0, FinalizeIAP)
227 |
228 | #endif // DM_PLATFORM_HTML5
229 |
--------------------------------------------------------------------------------
/extension-iap/src/iap_ios.mm:
--------------------------------------------------------------------------------
1 | #if defined(DM_PLATFORM_IOS)
2 |
3 | #include
4 |
5 | #include "iap.h"
6 | #include "iap_private.h"
7 |
8 | #import
9 | #import
10 | #import
11 |
12 | #define LIB_NAME "iap"
13 |
14 | struct IAP;
15 |
16 | @interface SKPaymentTransactionObserver : NSObject
17 | @property IAP* m_IAP;
18 | @end
19 |
20 | struct IAP
21 | {
22 | IAP()
23 | {
24 | memset(this, 0, sizeof(*this));
25 | m_AutoFinishTransactions = true;
26 | }
27 | int m_Version;
28 | bool m_AutoFinishTransactions;
29 | NSMutableDictionary* m_PendingTransactions;
30 | dmScript::LuaCallbackInfo* m_Listener;
31 | IAPCommandQueue m_CommandQueue;
32 | IAPCommandQueue m_ObservableQueue;
33 | SKPaymentTransactionObserver* m_Observer;
34 | };
35 |
36 | IAP g_IAP;
37 |
38 |
39 | // The payload of the purchasing commands
40 | struct IAPProduct
41 | {
42 | const char* ident;
43 | const char* title;
44 | const char* description;
45 | float price;
46 | const char* price_string;
47 | const char* currency_code;
48 | };
49 |
50 | struct IAPResponse
51 | {
52 | const char* error; // optional
53 | int32_t error_code; // only valid if error is set
54 | dmArray m_Products;
55 |
56 | IAPResponse() {
57 | memset(this, 0, sizeof(*this));
58 | }
59 | };
60 |
61 | struct IAPTransaction
62 | {
63 | const char* ident;
64 | int32_t state;
65 | const char* date;
66 | const char* trans_ident; // optional
67 | const char* receipt; // optional
68 | uint32_t receipt_length;
69 | const char* original_trans; // optional
70 | const char* error; // optional
71 | int32_t error_code; // only valid if error is set
72 | IAPTransaction* m_OriginalTransaction;
73 |
74 | IAPTransaction() {
75 | memset(this, 0, sizeof(*this));
76 | }
77 | };
78 |
79 | static void IAP_FreeProduct(IAPProduct* product)
80 | {
81 | free((void*)product->ident);
82 | free((void*)product->title);
83 | free((void*)product->description);
84 | free((void*)product->price_string);
85 | free((void*)product->currency_code);
86 | }
87 |
88 | static void IAP_FreeTransaction(IAPTransaction* transaction)
89 | {
90 | if (transaction->m_OriginalTransaction) {
91 | IAP_FreeTransaction(transaction->m_OriginalTransaction);
92 | }
93 | delete transaction->m_OriginalTransaction;
94 | free((void*)transaction->ident);
95 | free((void*)transaction->date);
96 | free((void*)transaction->trans_ident);
97 | free((void*)transaction->receipt);
98 | free((void*)transaction->original_trans);
99 | free((void*)transaction->error);
100 | }
101 |
102 |
103 | @interface SKProductsRequestDelegate : NSObject
104 | @property dmScript::LuaCallbackInfo* m_Callback;
105 | @property (assign) SKProductsRequest* m_Request;
106 | @property int m_Version;
107 | @end
108 |
109 | @implementation SKProductsRequestDelegate
110 | - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
111 |
112 | if (self.m_Version != g_IAP.m_Version) {
113 | dmLogWarning("Received products but the extension has been restarted")
114 | return;
115 | }
116 |
117 | if (!dmScript::IsCallbackValid(self.m_Callback)) {
118 | dmLogError("No callback set");
119 | return;
120 | }
121 |
122 | NSArray* skProducts = response.products;
123 |
124 | IAPResponse* iap_response = new IAPResponse;
125 | for (SKProduct * p in skProducts) {
126 |
127 | IAPProduct product = {0};
128 | product.ident = strdup([p.productIdentifier UTF8String]);
129 | if (p.localizedTitle) {
130 | product.title = strdup([p.localizedTitle UTF8String]);
131 | }
132 | else {
133 | dmLogWarning("Product %s has no localizedTitle", [p.productIdentifier UTF8String]);
134 | product.title = strdup("");
135 | }
136 | if (p.localizedDescription) {
137 | product.description = strdup([p.localizedDescription UTF8String]);
138 | }
139 | else {
140 | dmLogWarning("Product %s has no localizedDescription", [p.productIdentifier UTF8String]);
141 | product.description = strdup("");
142 | }
143 | product.currency_code = strdup([[p.priceLocale objectForKey:NSLocaleCurrencyCode] UTF8String]);
144 | product.price = p.price.floatValue;
145 |
146 | NSNumberFormatter *formatter = [[[NSNumberFormatter alloc] init] autorelease];
147 | [formatter setNumberStyle: NSNumberFormatterCurrencyStyle];
148 | [formatter setLocale: p.priceLocale];
149 | NSString* price_string = [formatter stringFromNumber: p.price];
150 | product.price_string = strdup([price_string UTF8String]);
151 |
152 | if (iap_response->m_Products.Full()) {
153 | iap_response->m_Products.OffsetCapacity(2);
154 | }
155 | iap_response->m_Products.Push(product);
156 | }
157 |
158 |
159 | IAPCommand cmd;
160 | cmd.m_Command = IAP_PRODUCT_RESULT;
161 | cmd.m_Callback = self.m_Callback;
162 | cmd.m_Data = iap_response;
163 | IAP_Queue_Push(&g_IAP.m_CommandQueue, &cmd);
164 | }
165 |
166 | static void HandleProductResult(IAPCommand* cmd)
167 | {
168 |
169 | IAPResponse* response = (IAPResponse*)cmd->m_Data;
170 |
171 | lua_State* L = dmScript::GetCallbackLuaContext(cmd->m_Callback);
172 | int top = lua_gettop(L);
173 |
174 | if (!dmScript::SetupCallback(cmd->m_Callback))
175 | {
176 | assert(top == lua_gettop(L));
177 | delete response;
178 | return;
179 | }
180 |
181 | lua_newtable(L);
182 |
183 | for (uint32_t i = 0; i < response->m_Products.Size(); ++i) {
184 | IAPProduct* product = &response->m_Products[i];
185 |
186 | lua_pushstring(L, product->ident);
187 | lua_newtable(L);
188 |
189 | lua_pushstring(L, product->ident);
190 | lua_setfield(L, -2, "ident");
191 | lua_pushstring(L, product->title);
192 | lua_setfield(L, -2, "title");
193 | lua_pushstring(L, product->description);
194 | lua_setfield(L, -2, "description");
195 | lua_pushnumber(L, product->price);
196 | lua_setfield(L, -2, "price");
197 | lua_pushstring(L, product->price_string);
198 | lua_setfield(L, -2, "price_string");
199 | lua_pushstring(L, product->currency_code);
200 | lua_setfield(L, -2, "currency_code");
201 |
202 | lua_rawset(L, -3);
203 |
204 | IAP_FreeProduct(product);
205 | }
206 | lua_pushnil(L);
207 |
208 | dmScript::PCall(L, 3, 0);
209 |
210 | dmScript::TeardownCallback(cmd->m_Callback);
211 | dmScript::DestroyCallback(cmd->m_Callback);
212 |
213 | delete response;
214 | assert(top == lua_gettop(L));
215 | }
216 |
217 | - (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
218 | dmLogWarning("SKProductsRequest failed: %s", [error.localizedDescription UTF8String]);
219 |
220 | if (!dmScript::IsCallbackValid(self.m_Callback)) {
221 | dmLogError("No callback set");
222 | return;
223 | }
224 |
225 | IAPResponse* response = new IAPResponse;
226 | response->error = strdup([error.localizedDescription UTF8String]);
227 | response->error_code = REASON_UNSPECIFIED;
228 |
229 | IAPCommand cmd;
230 | cmd.m_Command = IAP_PRODUCT_RESULT;
231 | cmd.m_Callback = self.m_Callback;
232 | cmd.m_Data = response;
233 |
234 | IAP_Queue_Push(&g_IAP.m_CommandQueue, &cmd);
235 | }
236 |
237 | - (void)requestDidFinish:(SKRequest *)request
238 | {
239 | [self.m_Request release];
240 | [self release];
241 | }
242 |
243 | @end
244 |
245 | static void CopyTransaction(SKPaymentTransaction* transaction, IAPTransaction* out)
246 | {
247 | if (transaction.transactionState == SKPaymentTransactionStateFailed) {
248 | out->error = strdup([transaction.error.localizedDescription UTF8String]);
249 | out->error_code = transaction.error.code == SKErrorPaymentCancelled ? REASON_USER_CANCELED : REASON_UNSPECIFIED;
250 | } else {
251 | out->error = 0;
252 | }
253 |
254 | NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
255 | [dateFormatter setLocale: [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
256 | [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZ"];
257 |
258 | out->ident = strdup([transaction.payment.productIdentifier UTF8String]);
259 |
260 |
261 | if (transaction.transactionDate)
262 | out->date = strdup([[dateFormatter stringFromDate: transaction.transactionDate] UTF8String]);
263 | out->state = transaction.transactionState;
264 |
265 | if (transaction.transactionState == SKPaymentTransactionStatePurchased ||
266 | transaction.transactionState == SKPaymentTransactionStateRestored) {
267 | out->trans_ident = strdup([transaction.transactionIdentifier UTF8String]);
268 | }
269 |
270 | if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
271 | NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
272 | NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
273 | out->receipt_length = receiptData.length;
274 | out->receipt = (const char*)malloc(out->receipt_length);
275 | memcpy((void*)out->receipt, receiptData.bytes, out->receipt_length);
276 | }
277 |
278 | if (transaction.transactionState == SKPaymentTransactionStateRestored && transaction.originalTransaction) {
279 | out->m_OriginalTransaction = new IAPTransaction;
280 | CopyTransaction(transaction.originalTransaction, out->m_OriginalTransaction);
281 | }
282 | }
283 |
284 | static void PushTransaction(lua_State* L, IAPTransaction* transaction)
285 | {
286 | // first argument to the callback
287 | lua_newtable(L);
288 |
289 | lua_pushstring(L, transaction->ident);
290 | lua_setfield(L, -2, "ident");
291 | lua_pushnumber(L, transaction->state);
292 | lua_setfield(L, -2, "state");
293 | if (transaction->date) {
294 | lua_pushstring(L, transaction->date);
295 | lua_setfield(L, -2, "date");
296 | }
297 |
298 | if (transaction->trans_ident) {
299 | lua_pushstring(L, transaction->trans_ident);
300 | lua_setfield(L, -2, "trans_ident");
301 | }
302 | if (transaction->receipt) {
303 | lua_pushlstring(L, transaction->receipt, transaction->receipt_length);
304 | lua_setfield(L, -2, "receipt");
305 | }
306 |
307 | if (transaction->m_OriginalTransaction) {
308 | lua_pushstring(L, "original_trans");
309 | PushTransaction(L, transaction->m_OriginalTransaction);
310 | lua_rawset(L, -3);
311 | }
312 | }
313 |
314 |
315 | static void HandlePurchaseResult(IAPCommand* cmd)
316 | {
317 | IAPTransaction* transaction = (IAPTransaction*)cmd->m_Data;
318 |
319 | lua_State* L = dmScript::GetCallbackLuaContext(cmd->m_Callback);
320 | int top = lua_gettop(L);
321 |
322 | if (!dmScript::SetupCallback(cmd->m_Callback))
323 | {
324 | assert(top == lua_gettop(L));
325 | return;
326 | }
327 |
328 | PushTransaction(L, transaction);
329 |
330 | // Second argument to callback
331 | if (transaction->error) {
332 | IAP_PushError(L, transaction->error, transaction->error_code);
333 | } else {
334 | lua_pushnil(L);
335 | }
336 |
337 | dmScript::PCall(L, 3, 0);
338 |
339 | dmScript::TeardownCallback(cmd->m_Callback);
340 |
341 | IAP_FreeTransaction(transaction);
342 |
343 | delete transaction;
344 | assert(top == lua_gettop(L));
345 | }
346 |
347 | static void processTransactions(IAP* iap, NSArray* transactions) {
348 | for (SKPaymentTransaction* transaction in transactions) {
349 | if ((!iap->m_AutoFinishTransactions) && (transaction.transactionState == SKPaymentTransactionStatePurchased)) {
350 | NSData *data = [transaction.transactionIdentifier dataUsingEncoding:NSUTF8StringEncoding];
351 | uint64_t trans_id_hash = dmHashBuffer64((const char*) [data bytes], [data length]);
352 | [iap->m_PendingTransactions setObject:transaction forKey:[NSNumber numberWithInteger:trans_id_hash] ];
353 | }
354 |
355 | if (!iap->m_Listener)
356 | continue;
357 |
358 | IAPTransaction* iap_transaction = new IAPTransaction;
359 | CopyTransaction(transaction, iap_transaction);
360 |
361 | IAPCommand cmd;
362 | cmd.m_Callback = iap->m_Listener;
363 | cmd.m_Command = IAP_PURCHASE_RESULT;
364 | cmd.m_Data = iap_transaction;
365 | IAP_Queue_Push(&iap->m_ObservableQueue, &cmd);
366 |
367 | switch (transaction.transactionState)
368 | {
369 | case SKPaymentTransactionStatePurchased:
370 | if (g_IAP.m_AutoFinishTransactions) {
371 | [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
372 | }
373 | break;
374 | case SKPaymentTransactionStateFailed:
375 | [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
376 | break;
377 | case SKPaymentTransactionStateRestored:
378 | [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
379 | break;
380 | default:
381 | break;
382 | }
383 | }
384 | }
385 |
386 | static int IAP_ProcessPendingTransactions(lua_State* L)
387 | {
388 | processTransactions(&g_IAP, [SKPaymentQueue defaultQueue].transactions);
389 | return 0;
390 | }
391 |
392 | @implementation SKPaymentTransactionObserver
393 | - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
394 | {
395 | processTransactions(self.m_IAP, transactions);
396 | }
397 | @end
398 |
399 | static int IAP_List(lua_State* L)
400 | {
401 | int top = lua_gettop(L);
402 |
403 | NSCountedSet* product_identifiers = [[[NSCountedSet alloc] init] autorelease];
404 |
405 | luaL_checktype(L, 1, LUA_TTABLE);
406 | lua_pushnil(L);
407 | while (lua_next(L, 1) != 0) {
408 | const char* p = luaL_checkstring(L, -1);
409 | [product_identifiers addObject: [NSString stringWithUTF8String: p]];
410 | lua_pop(L, 1);
411 | }
412 |
413 | SKProductsRequest* products_request = [[SKProductsRequest alloc] initWithProductIdentifiers: product_identifiers];
414 | SKProductsRequestDelegate* delegate = [SKProductsRequestDelegate alloc];
415 |
416 | delegate.m_Callback = dmScript::CreateCallback(L, 2);
417 | delegate.m_Request = products_request;
418 | delegate.m_Version = g_IAP.m_Version;
419 | products_request.delegate = delegate;
420 | [products_request start];
421 |
422 | assert(top == lua_gettop(L));
423 | return 0;
424 | }
425 |
426 | static int IAP_Buy(lua_State* L)
427 | {
428 | int top = lua_gettop(L);
429 |
430 | const char* id = luaL_checkstring(L, 1);
431 | SKMutablePayment* payment = [[SKMutablePayment alloc] init];
432 | payment.productIdentifier = [NSString stringWithUTF8String: id];
433 | payment.quantity = 1;
434 |
435 | [[SKPaymentQueue defaultQueue] addPayment:payment];
436 | [payment release];
437 |
438 | assert(top == lua_gettop(L));
439 | return 0;
440 | }
441 |
442 | static int IAP_Finish(lua_State* L)
443 | {
444 | if(g_IAP.m_AutoFinishTransactions)
445 | {
446 | dmLogWarning("Calling iap.finish when autofinish transactions is enabled. Ignored.");
447 | return 0;
448 | }
449 |
450 | int top = lua_gettop(L);
451 |
452 | luaL_checktype(L, 1, LUA_TTABLE);
453 |
454 | lua_getfield(L, -1, "state");
455 | if (lua_isnumber(L, -1))
456 | {
457 | if(lua_tointeger(L, -1) != SKPaymentTransactionStatePurchased)
458 | {
459 | dmLogError("Transaction error. Invalid transaction state for transaction finish (must be iap.TRANS_STATE_PURCHASED).");
460 | lua_pop(L, 1);
461 | assert(top == lua_gettop(L));
462 | return 0;
463 | }
464 | }
465 | lua_pop(L, 1);
466 |
467 | lua_getfield(L, -1, "trans_ident");
468 | if (!lua_isstring(L, -1)) {
469 | dmLogError("Transaction error. Invalid transaction data for transaction finish, does not contain 'trans_ident' key.");
470 | lua_pop(L, 1);
471 | }
472 | else
473 | {
474 | const char *str = lua_tostring(L, -1);
475 | uint64_t trans_ident_hash = dmHashBuffer64(str, strlen(str));
476 | lua_pop(L, 1);
477 | SKPaymentTransaction * transaction = [g_IAP.m_PendingTransactions objectForKey:[NSNumber numberWithInteger:trans_ident_hash]];
478 | if(transaction == 0x0) {
479 | dmLogError("Transaction error. Invalid trans_ident value for transaction finish.");
480 | } else {
481 | [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
482 | [g_IAP.m_PendingTransactions removeObjectForKey:[NSNumber numberWithInteger:trans_ident_hash]];
483 | }
484 | }
485 |
486 | assert(top == lua_gettop(L));
487 | return 0;
488 | }
489 |
490 | static int IAP_Restore(lua_State* L)
491 | {
492 | // TODO: Missing callback here for completion/error
493 | // See callback under "Handling Restored Transactions"
494 | // https://developer.apple.com/library/ios/documentation/StoreKit/Reference/SKPaymentTransactionObserver_Protocol/Reference/Reference.html
495 | int top = lua_gettop(L);
496 | [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
497 | assert(top == lua_gettop(L));
498 | lua_pushboolean(L, 1);
499 | return 1;
500 | }
501 |
502 | static int IAP_SetListener(lua_State* L)
503 | {
504 | IAP* iap = &g_IAP;
505 |
506 | if (iap->m_Listener)
507 | dmScript::DestroyCallback(iap->m_Listener);
508 |
509 | iap->m_Listener = dmScript::CreateCallback(L, 1);
510 | return 0;
511 | }
512 |
513 | static int IAP_Acknowledge(lua_State* L)
514 | {
515 | return 0;
516 | }
517 |
518 | static int IAP_GetProviderId(lua_State* L)
519 | {
520 | lua_pushinteger(L, PROVIDER_ID_APPLE);
521 | return 1;
522 | }
523 |
524 | static const luaL_reg IAP_methods[] =
525 | {
526 | {"list", IAP_List},
527 | {"buy", IAP_Buy},
528 | {"finish", IAP_Finish},
529 | {"acknowledge", IAP_Acknowledge},
530 | {"restore", IAP_Restore},
531 | {"set_listener", IAP_SetListener},
532 | {"get_provider_id", IAP_GetProviderId},
533 | {"process_pending_transactions", IAP_ProcessPendingTransactions},
534 | {0, 0}
535 | };
536 |
537 | static dmExtension::Result InitializeIAP(dmExtension::Params* params)
538 | {
539 | g_IAP.m_AutoFinishTransactions = dmConfigFile::GetInt(params->m_ConfigFile, "iap.auto_finish_transactions", 1) == 1;
540 | g_IAP.m_PendingTransactions = [[NSMutableDictionary alloc]initWithCapacity:2];
541 | g_IAP.m_Version++;
542 |
543 | IAP_Queue_Create(&g_IAP.m_CommandQueue);
544 | IAP_Queue_Create(&g_IAP.m_ObservableQueue);
545 |
546 | lua_State*L = params->m_L;
547 | int top = lua_gettop(L);
548 | luaL_register(L, LIB_NAME, IAP_methods);
549 |
550 | // ensure ios payment constants values corresponds to iap constants.
551 | assert((int)TRANS_STATE_PURCHASING == (int)SKPaymentTransactionStatePurchasing);
552 | assert((int)TRANS_STATE_PURCHASED == (int)SKPaymentTransactionStatePurchased);
553 | assert((int)TRANS_STATE_FAILED == (int)SKPaymentTransactionStateFailed);
554 | assert((int)TRANS_STATE_RESTORED == (int)SKPaymentTransactionStateRestored);
555 |
556 | IAP_PushConstants(L);
557 |
558 | lua_pop(L, 1);
559 | assert(top == lua_gettop(L));
560 |
561 | SKPaymentTransactionObserver* observer = [[SKPaymentTransactionObserver alloc] init];
562 | observer.m_IAP = &g_IAP;
563 | [[SKPaymentQueue defaultQueue] addTransactionObserver: observer];
564 | g_IAP.m_Observer = observer;
565 |
566 | return dmExtension::RESULT_OK;
567 | }
568 |
569 | static void IAP_OnCommand(IAPCommand* cmd, void*)
570 | {
571 | switch (cmd->m_Command)
572 | {
573 | case IAP_PRODUCT_RESULT:
574 | HandleProductResult(cmd);
575 | break;
576 | case IAP_PURCHASE_RESULT:
577 | HandlePurchaseResult(cmd);
578 | break;
579 |
580 | default:
581 | assert(false);
582 | }
583 | }
584 |
585 | static dmExtension::Result UpdateIAP(dmExtension::Params* params)
586 | {
587 | IAP_Queue_Flush(&g_IAP.m_CommandQueue, IAP_OnCommand, 0);
588 | if (g_IAP.m_Observer) {
589 | IAP_Queue_Flush(&g_IAP.m_ObservableQueue, IAP_OnCommand, 0);
590 | }
591 | return dmExtension::RESULT_OK;
592 | }
593 |
594 | static dmExtension::Result FinalizeIAP(dmExtension::Params* params)
595 | {
596 | if (g_IAP.m_Listener)
597 | {
598 | dmScript::DestroyCallback(g_IAP.m_Listener);
599 | }
600 |
601 | g_IAP.m_Listener = 0;
602 |
603 | if (g_IAP.m_PendingTransactions) {
604 | [g_IAP.m_PendingTransactions release];
605 | g_IAP.m_PendingTransactions = 0;
606 | }
607 |
608 | if (g_IAP.m_Observer) {
609 | [[SKPaymentQueue defaultQueue] removeTransactionObserver: g_IAP.m_Observer];
610 | [g_IAP.m_Observer release];
611 | g_IAP.m_Observer = 0;
612 | }
613 |
614 | IAP_Queue_Destroy(&g_IAP.m_CommandQueue);
615 | IAP_Queue_Destroy(&g_IAP.m_ObservableQueue);
616 |
617 | return dmExtension::RESULT_OK;
618 | }
619 |
620 |
621 | DM_DECLARE_EXTENSION(IAPExt, "IAP", 0, 0, InitializeIAP, UpdateIAP, 0, FinalizeIAP)
622 |
623 | #endif // DM_PLATFORM_IOS
624 |
--------------------------------------------------------------------------------
/extension-iap/src/iap_null.cpp:
--------------------------------------------------------------------------------
1 | #if !defined(DM_PLATFORM_HTML5) && !defined(DM_PLATFORM_ANDROID) && !defined(DM_PLATFORM_IOS)
2 |
3 | extern "C" void IAPExt()
4 | {
5 |
6 | }
7 |
8 | #endif // !DM_PLATFORM_HTML5 && !DM_PLATFORM_ANDROID && !DM_PLATFORM_IOS
9 |
10 |
--------------------------------------------------------------------------------
/extension-iap/src/iap_private.cpp:
--------------------------------------------------------------------------------
1 | #if defined(DM_PLATFORM_HTML5) || defined(DM_PLATFORM_ANDROID) || defined(DM_PLATFORM_IOS)
2 |
3 | #include
4 |
5 | #include "iap.h"
6 | #include "iap_private.h"
7 | #include
8 | #include
9 |
10 | // Creates a comma separated string, given a table where all values are strings (or numbers)
11 | // Returns a malloc'ed string, which the caller must free
12 | char* IAP_List_CreateBuffer(lua_State* L)
13 | {
14 | int top = lua_gettop(L);
15 |
16 | luaL_checktype(L, 1, LUA_TTABLE);
17 | lua_pushnil(L);
18 | int length = 0;
19 | while (lua_next(L, 1) != 0) {
20 | if (length > 0) {
21 | ++length;
22 | }
23 | const char* p = lua_tostring(L, -1);
24 | if(!p)
25 | {
26 | luaL_error(L, "IAP: Failed to get value (string) from table");
27 | }
28 | length += strlen(p);
29 | lua_pop(L, 1);
30 | }
31 |
32 | char* buf = (char*)malloc(length+1);
33 | if( buf == 0 )
34 | {
35 | dmLogError("Could not allocate buffer of size %d", length+1);
36 | assert(top == lua_gettop(L));
37 | return 0;
38 | }
39 | buf[0] = '\0';
40 |
41 | int i = 0;
42 | lua_pushnil(L);
43 | while (lua_next(L, 1) != 0) {
44 | if (i > 0) {
45 | dmStrlCat(buf, ",", length+1);
46 | }
47 | const char* p = lua_tostring(L, -1);
48 | if(!p)
49 | {
50 | luaL_error(L, "IAP: Failed to get value (string) from table");
51 | }
52 | dmStrlCat(buf, p, length+1);
53 | lua_pop(L, 1);
54 | ++i;
55 | }
56 |
57 | assert(top == lua_gettop(L));
58 | return buf;
59 | }
60 |
61 | void IAP_PushError(lua_State* L, const char* error, int reason)
62 | {
63 | if (error != 0) {
64 | lua_newtable(L);
65 | lua_pushstring(L, "error");
66 | lua_pushstring(L, error);
67 | lua_rawset(L, -3);
68 | lua_pushstring(L, "reason");
69 | lua_pushnumber(L, reason);
70 | lua_rawset(L, -3);
71 | } else {
72 | lua_pushnil(L);
73 | }
74 | }
75 |
76 | void IAP_PushConstants(lua_State* L)
77 | {
78 | #define SETCONSTANT(name) \
79 | lua_pushnumber(L, (lua_Number) name); \
80 | lua_setfield(L, -2, #name);\
81 |
82 | SETCONSTANT(TRANS_STATE_PURCHASING)
83 | SETCONSTANT(TRANS_STATE_PURCHASED)
84 | SETCONSTANT(TRANS_STATE_FAILED)
85 | SETCONSTANT(TRANS_STATE_RESTORED)
86 | SETCONSTANT(TRANS_STATE_UNVERIFIED)
87 |
88 | SETCONSTANT(REASON_UNSPECIFIED)
89 | SETCONSTANT(REASON_USER_CANCELED)
90 |
91 | SETCONSTANT(PROVIDER_ID_GOOGLE)
92 | SETCONSTANT(PROVIDER_ID_AMAZON)
93 | SETCONSTANT(PROVIDER_ID_APPLE)
94 | SETCONSTANT(PROVIDER_ID_FACEBOOK)
95 |
96 | #undef SETCONSTANT
97 | }
98 |
99 |
100 | void IAP_Queue_Create(IAPCommandQueue* queue)
101 | {
102 | queue->m_Mutex = dmMutex::New();
103 | }
104 |
105 | void IAP_Queue_Destroy(IAPCommandQueue* queue)
106 | {
107 | dmMutex::Delete(queue->m_Mutex);
108 | }
109 |
110 | void IAP_Queue_Push(IAPCommandQueue* queue, IAPCommand* cmd)
111 | {
112 | DM_MUTEX_SCOPED_LOCK(queue->m_Mutex);
113 |
114 | if(queue->m_Commands.Full())
115 | {
116 | queue->m_Commands.OffsetCapacity(2);
117 | }
118 | queue->m_Commands.Push(*cmd);
119 | }
120 |
121 | void IAP_Queue_Flush(IAPCommandQueue* queue, IAPCommandFn fn, void* ctx)
122 | {
123 | assert(fn != 0);
124 |
125 | if (queue->m_Commands.Empty())
126 | {
127 | return;
128 | }
129 |
130 | dmArray tmp;
131 | {
132 | DM_MUTEX_SCOPED_LOCK(queue->m_Mutex);
133 | tmp.Swap(queue->m_Commands);
134 | }
135 |
136 | for(uint32_t i = 0; i != tmp.Size(); ++i)
137 | {
138 | fn(&tmp[i], ctx);
139 | }
140 | }
141 |
142 | #endif // DM_PLATFORM_HTML5 || DM_PLATFORM_ANDROID || DM_PLATFORM_IOS
143 |
--------------------------------------------------------------------------------
/extension-iap/src/iap_private.h:
--------------------------------------------------------------------------------
1 | #if defined(DM_PLATFORM_HTML5) || defined(DM_PLATFORM_ANDROID) || defined(DM_PLATFORM_IOS)
2 |
3 | #ifndef IAP_PRIVATE_H
4 | #define IAP_PRIVATE_H
5 |
6 | #include
7 |
8 | enum EIAPCommand
9 | {
10 | IAP_PRODUCT_RESULT,
11 | IAP_PURCHASE_RESULT,
12 | };
13 |
14 | struct DM_ALIGNED(16) IAPCommand
15 | {
16 | IAPCommand()
17 | {
18 | memset(this, 0, sizeof(IAPCommand));
19 | }
20 |
21 | // Used for storing eventual callback info (if needed)
22 | dmScript::LuaCallbackInfo* m_Callback;
23 |
24 | // The actual command payload
25 | int32_t m_Command;
26 | int32_t m_ResponseCode;
27 | void* m_Data;
28 | };
29 |
30 | struct IAPCommandQueue
31 | {
32 | dmArray m_Commands;
33 | dmMutex::HMutex m_Mutex;
34 | };
35 |
36 | char* IAP_List_CreateBuffer(lua_State* L);
37 | void IAP_PushError(lua_State* L, const char* error, int reason);
38 | void IAP_PushConstants(lua_State* L);
39 |
40 | typedef void (*IAPCommandFn)(IAPCommand* cmd, void* ctx);
41 |
42 | void IAP_Queue_Create(IAPCommandQueue* queue);
43 | void IAP_Queue_Destroy(IAPCommandQueue* queue);
44 | // The command is copied by value into the queue
45 | void IAP_Queue_Push(IAPCommandQueue* queue, IAPCommand* cmd);
46 | void IAP_Queue_Flush(IAPCommandQueue* queue, IAPCommandFn fn, void* ctx);
47 |
48 | #endif
49 |
50 | #endif // DM_PLATFORM_HTML5 || DM_PLATFORM_ANDROID || DM_PLATFORM_IOS
51 |
--------------------------------------------------------------------------------
/extension-iap/src/java/com/defold/iap/IListProductsListener.java:
--------------------------------------------------------------------------------
1 | package com.defold.iap;
2 |
3 | public interface IListProductsListener {
4 | public void onProductsResult(int resultCode, String productList, long cmdHandle);
5 | }
6 |
--------------------------------------------------------------------------------
/extension-iap/src/java/com/defold/iap/IPurchaseListener.java:
--------------------------------------------------------------------------------
1 | package com.defold.iap;
2 |
3 | public interface IPurchaseListener {
4 | public void onPurchaseResult(int responseCode, String purchaseData);
5 | }
6 |
--------------------------------------------------------------------------------
/extension-iap/src/java/com/defold/iap/IapAmazon.java:
--------------------------------------------------------------------------------
1 | package com.defold.iap;
2 |
3 | import java.text.SimpleDateFormat;
4 | import java.util.Map;
5 | import java.util.Set;
6 | import java.util.HashMap;
7 | import java.util.HashSet;
8 | import java.util.Date;
9 | import java.util.concurrent.ArrayBlockingQueue;
10 | import java.util.concurrent.BlockingQueue;
11 |
12 | import org.json.JSONException;
13 | import org.json.JSONObject;
14 | import org.json.JSONArray;
15 |
16 | import android.app.Activity;
17 | import android.os.Bundle;
18 | import android.os.IBinder;
19 | import android.os.RemoteException;
20 | import android.util.Log;
21 |
22 | import com.amazon.device.iap.PurchasingService;
23 | import com.amazon.device.iap.PurchasingListener;
24 | import com.amazon.device.iap.model.ProductDataResponse;
25 | import com.amazon.device.iap.model.PurchaseUpdatesResponse;
26 | import com.amazon.device.iap.model.PurchaseResponse;
27 | import com.amazon.device.iap.model.UserDataResponse;
28 | import com.amazon.device.iap.model.RequestId;
29 | import com.amazon.device.iap.model.Product;
30 | import com.amazon.device.iap.model.Receipt;
31 | import com.amazon.device.iap.model.UserData;
32 | import com.amazon.device.iap.model.FulfillmentResult;
33 |
34 | public class IapAmazon implements PurchasingListener {
35 |
36 | public static final String TAG = "iap";
37 |
38 | private HashMap listProductsListeners;
39 | private HashMap listProductsCommandPtrs;
40 | private HashMap purchaseListeners;
41 |
42 | private Activity activity;
43 | private boolean autoFinishTransactions;
44 |
45 | public IapAmazon(Activity activity, boolean autoFinishTransactions) {
46 | this.activity = activity;
47 | this.autoFinishTransactions = autoFinishTransactions;
48 | this.listProductsListeners = new HashMap();
49 | this.listProductsCommandPtrs = new HashMap();
50 | this.purchaseListeners = new HashMap();
51 | PurchasingService.registerListener(activity, this);
52 | }
53 |
54 | private void init() {
55 | }
56 |
57 | public void stop() {
58 | }
59 |
60 | public void listItems(final String skus, final IListProductsListener listener, final long commandPtr) {
61 | final Set skuSet = new HashSet();
62 | for (String x : skus.split(",")) {
63 | if (x.trim().length() > 0) {
64 | if (!skuSet.contains(x)) {
65 | skuSet.add(x);
66 | }
67 | }
68 | }
69 |
70 | // It might seem unconventional to hold the lock while doing the function call,
71 | // but it prevents a race condition, as the API does not allow supplying own
72 | // requestId which could be generated ahead of time.
73 | synchronized (listProductsListeners) {
74 | RequestId req = PurchasingService.getProductData(skuSet);
75 | if (req != null) {
76 | listProductsListeners.put(req, listener);
77 | listProductsCommandPtrs.put(req, commandPtr);
78 | } else {
79 | Log.e(TAG, "Did not expect a null requestId");
80 | }
81 | }
82 | }
83 |
84 | public void buy(final String product, final String token, final IPurchaseListener listener) {
85 | synchronized (purchaseListeners) {
86 | RequestId req = PurchasingService.purchase(product);
87 | if (req != null) {
88 | purchaseListeners.put(req, listener);
89 | } else {
90 | Log.e(TAG, "Did not expect a null requestId");
91 | }
92 | }
93 | }
94 |
95 | public void finishTransaction(final String receipt, final IPurchaseListener listener) {
96 | if(this.autoFinishTransactions) {
97 | return;
98 | }
99 | PurchasingService.notifyFulfillment(receipt, FulfillmentResult.FULFILLED);
100 | }
101 |
102 | public void acknowledgeTransaction(final String purchaseToken, final IPurchaseListener purchaseListener) {
103 | // Stub to prevent errors.
104 | }
105 |
106 | private void doGetPurchaseUpdates(final IPurchaseListener listener, final boolean reset) {
107 | synchronized (purchaseListeners) {
108 | RequestId req = PurchasingService.getPurchaseUpdates(reset);
109 | if (req != null) {
110 | purchaseListeners.put(req, listener);
111 | } else {
112 | Log.e(TAG, "Did not expect a null requestId");
113 | }
114 | }
115 | }
116 |
117 | public void processPendingConsumables(final IPurchaseListener listener) {
118 | // reset = false means getting any new receipts since the last call.
119 | doGetPurchaseUpdates(listener, false);
120 | }
121 |
122 | public void restore(final IPurchaseListener listener) {
123 | // reset = true means getting all transaction history, although consumables
124 | // are not included, only entitlements, after testing.
125 | doGetPurchaseUpdates(listener, true);
126 | }
127 |
128 | public static String toISO8601(final Date date) {
129 | String formatted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(date);
130 | return formatted.substring(0, 22) + ":" + formatted.substring(22);
131 | }
132 |
133 | private JSONObject makeTransactionObject(final UserData user, final Receipt receipt, int state) throws JSONException {
134 | JSONObject transaction = new JSONObject();
135 | transaction.put("ident", receipt.getSku());
136 | transaction.put("state", state);
137 | transaction.put("date", toISO8601(receipt.getPurchaseDate()));
138 | transaction.put("trans_ident", receipt.getReceiptId());
139 | transaction.put("receipt", receipt.getReceiptId());
140 |
141 | // Only for amazon (this far), but required for using their server side receipt validation.
142 | transaction.put("is_sandbox_mode", PurchasingService.IS_SANDBOX_MODE);
143 | transaction.put("user_id", user.getUserId());
144 |
145 | // According to documentation, cancellation support has to be enabled per item, and this is
146 | // not officially supported by any other IAP provider, and it is not expected to be used here either.
147 | //
148 | // But enforcing the use of only non-cancelable items is not possible either; so include these flags
149 | // for completeness.
150 | if (receipt.getCancelDate() != null)
151 | transaction.put("cancel_date", toISO8601(receipt.getCancelDate()));
152 | transaction.put("canceled", receipt.isCanceled());
153 | return transaction;
154 | }
155 |
156 | // This callback method is invoked when an ProductDataResponse is available for a request initiated by PurchasingService.getProductData(java.util.Set).
157 | @Override
158 | public void onProductDataResponse(ProductDataResponse productDataResponse) {
159 | RequestId reqId = productDataResponse.getRequestId();
160 | IListProductsListener listener;
161 | long commadPtr = 0;
162 | synchronized (this.listProductsListeners) {
163 | listener = this.listProductsListeners.get(reqId);
164 | commadPtr = this.listProductsCommandPtrs.get(reqId);
165 |
166 | this.listProductsListeners.remove(reqId);
167 | this.listProductsCommandPtrs.remove(reqId);
168 | if (listener == null) {
169 | Log.e(TAG, "No listener found for request " + reqId.toString());
170 | return;
171 | }
172 | }
173 |
174 | if (productDataResponse.getRequestStatus() != ProductDataResponse.RequestStatus.SUCCESSFUL) {
175 | listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null, commadPtr);
176 | } else {
177 | for (final String s : productDataResponse.getUnavailableSkus()) {
178 | Log.v(TAG, "Unavailable SKU: " + s);
179 | }
180 |
181 | Map products = productDataResponse.getProductData();
182 | try {
183 | JSONArray data = new JSONArray();
184 | for (Map.Entry entry : products.entrySet()) {
185 | String key = entry.getKey();
186 | Product product = entry.getValue();
187 | JSONObject item = new JSONObject();
188 | item.put("ident", product.getSku());
189 | item.put("title", product.getTitle());
190 | item.put("description", product.getDescription());
191 | if (product.getPrice() != null) {
192 | String priceString = product.getPrice();
193 | item.put("price_string", priceString);
194 | // Based on return values from getPrice: https://developer.amazon.com/public/binaries/content/assets/javadoc/in-app-purchasing-api/com/amazon/inapp/purchasing/item.html
195 | item.put("price", priceString.replaceAll("[^0-9.,]", ""));
196 | }
197 | data.put(item);
198 | }
199 | listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_OK, data.toString(), commadPtr);
200 | } catch (JSONException e) {
201 | listener.onProductsResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null, commadPtr);
202 | }
203 | }
204 | }
205 |
206 | // Convenience function for getting and removing a purchaseListener (used for more than one operation).
207 | private IPurchaseListener pickPurchaseListener(RequestId requestId) {
208 | synchronized (this.purchaseListeners) {
209 | IPurchaseListener listener = this.purchaseListeners.get(requestId);
210 | if (listener != null) {
211 | this.purchaseListeners.remove(requestId);
212 | return listener;
213 | }
214 | }
215 | return null;
216 | }
217 |
218 | // This callback method is invoked when a PurchaseResponse is available for a purchase initiated by PurchasingService.purchase(String).
219 | @Override
220 | public void onPurchaseResponse(PurchaseResponse purchaseResponse) {
221 |
222 | IPurchaseListener listener = pickPurchaseListener(purchaseResponse.getRequestId());
223 | if (listener == null) {
224 | Log.e(TAG, "No listener found for request: " + purchaseResponse.getRequestId().toString());
225 | return;
226 | }
227 |
228 | int code;
229 | String data = null;
230 | String fulfilReceiptId = null;
231 |
232 | switch (purchaseResponse.getRequestStatus()) {
233 | case SUCCESSFUL:
234 | {
235 | try {
236 | code = IapJNI.BILLING_RESPONSE_RESULT_OK;
237 | data = makeTransactionObject(purchaseResponse.getUserData(), purchaseResponse.getReceipt(), IapJNI.TRANS_STATE_PURCHASED).toString();
238 | fulfilReceiptId = purchaseResponse.getReceipt().getReceiptId();
239 | } catch (JSONException e) {
240 | Log.e(TAG, "JSON Exception occured: " + e.toString());
241 | code = IapJNI.BILLING_RESPONSE_RESULT_DEVELOPER_ERROR;
242 | }
243 | }
244 | break;
245 | case ALREADY_PURCHASED:
246 | code = IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED;
247 | break;
248 | case INVALID_SKU:
249 | code = IapJNI.BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE;
250 | break;
251 | case FAILED:
252 | case NOT_SUPPORTED:
253 | default:
254 | code = IapJNI.BILLING_RESPONSE_RESULT_ERROR;
255 | break;
256 | }
257 |
258 | listener.onPurchaseResult(code, data);
259 |
260 | if (fulfilReceiptId != null && autoFinishTransactions) {
261 | PurchasingService.notifyFulfillment(fulfilReceiptId, FulfillmentResult.FULFILLED);
262 | }
263 | }
264 |
265 | // This callback method is invoked when a PurchaseUpdatesResponse is available for a request initiated by PurchasingService.getPurchaseUpdates(boolean).
266 | @Override
267 | public void onPurchaseUpdatesResponse(PurchaseUpdatesResponse purchaseUpdatesResponse) {
268 |
269 | // The documentation seems to be a little misguiding regarding how to handle this.
270 | // This call is in response to getPurchaseUpdates() which can be called in two modes
271 | //
272 | // 1) Get all receipts since last call (reset = true)
273 | // 2) Get the whole transaction history.
274 | //
275 | // The result can carry the flag hasMore() where it is required to call getPurchaseUpdates again. See docs:
276 | // https://developer.amazon.com/public/apis/earn/in-app-purchasing/docs-v2/implementing-iap-2.0
277 | //
278 | // Examples indicate it should be called with the same value for 'reset' the secon time around
279 | // but actual testing ends up in an infinite loop where the same results are returned over and over.
280 | //
281 | // So here getPurchaseUpdates is called with result=false to fetch the next round of receipts.
282 |
283 | RequestId reqId = purchaseUpdatesResponse.getRequestId();
284 | IPurchaseListener listener = pickPurchaseListener(reqId);
285 | if (listener == null) {
286 | Log.e(TAG, "No listener found for request " + reqId.toString());
287 | return;
288 | }
289 |
290 | switch (purchaseUpdatesResponse.getRequestStatus()) {
291 | case SUCCESSFUL:
292 | {
293 | try {
294 | for (Receipt receipt : purchaseUpdatesResponse.getReceipts()) {
295 | JSONObject trans = makeTransactionObject(purchaseUpdatesResponse.getUserData(), receipt, IapJNI.TRANS_STATE_PURCHASED);
296 | listener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_OK, trans.toString());
297 | if(autoFinishTransactions) {
298 | PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED);
299 | }
300 | }
301 | if (purchaseUpdatesResponse.hasMore()) {
302 | doGetPurchaseUpdates(listener, false);
303 | }
304 | } catch (JSONException e) {
305 | Log.e(TAG, "JSON Exception occured: " + e.toString());
306 | listener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_DEVELOPER_ERROR, null);
307 | }
308 | }
309 | break;
310 | case FAILED:
311 | case NOT_SUPPORTED:
312 | default:
313 | listener.onPurchaseResult(IapJNI.BILLING_RESPONSE_RESULT_ERROR, null);
314 | break;
315 | }
316 | }
317 |
318 | // This callback method is invoked when a UserDataResponse is available for a request initiated by PurchasingService.getUserData().
319 | @Override
320 | public void onUserDataResponse(UserDataResponse userDataResponse) {
321 | // Intentionally left un-implemented; not used.
322 | }
323 | }
324 |
--------------------------------------------------------------------------------
/extension-iap/src/java/com/defold/iap/IapGooglePlay.java:
--------------------------------------------------------------------------------
1 | package com.defold.iap;
2 |
3 | import java.text.SimpleDateFormat;
4 | import java.util.ArrayList;
5 | import java.util.Date;
6 | import java.util.Iterator;
7 | import java.util.List;
8 | import java.util.Map;
9 | import java.util.HashMap;
10 | import java.util.concurrent.ArrayBlockingQueue;
11 | import java.util.concurrent.BlockingQueue;
12 |
13 | import org.json.JSONException;
14 | import org.json.JSONObject;
15 | import org.json.JSONArray;
16 |
17 | import android.app.Activity;
18 | import android.util.Log;
19 |
20 | import com.android.billingclient.api.BillingClient;
21 | import com.android.billingclient.api.BillingClient.BillingResponseCode;
22 | import com.android.billingclient.api.BillingClient.ProductType;
23 | import com.android.billingclient.api.BillingResult;
24 | import com.android.billingclient.api.PendingPurchasesParams;
25 | import com.android.billingclient.api.Purchase;
26 | import com.android.billingclient.api.Purchase.PurchaseState;
27 | import com.android.billingclient.api.ProductDetails;
28 | import com.android.billingclient.api.ProductDetails.OneTimePurchaseOfferDetails;
29 | import com.android.billingclient.api.ProductDetails.PricingPhases;
30 | import com.android.billingclient.api.ProductDetails.PricingPhase;
31 | import com.android.billingclient.api.ProductDetails.RecurrenceMode;
32 | import com.android.billingclient.api.ProductDetails.SubscriptionOfferDetails;
33 | import com.android.billingclient.api.ConsumeParams;
34 | import com.android.billingclient.api.BillingFlowParams;
35 | import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams;
36 | import com.android.billingclient.api.QueryPurchasesParams;
37 | import com.android.billingclient.api.QueryProductDetailsParams;
38 | import com.android.billingclient.api.QueryProductDetailsParams.Product;
39 | import com.android.billingclient.api.AcknowledgePurchaseParams;
40 | import com.android.billingclient.api.PurchasesUpdatedListener;
41 | import com.android.billingclient.api.BillingClientStateListener;
42 | import com.android.billingclient.api.ConsumeResponseListener;
43 | import com.android.billingclient.api.PurchasesResponseListener;
44 | import com.android.billingclient.api.ProductDetailsResponseListener;
45 | import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
46 |
47 | public class IapGooglePlay implements PurchasesUpdatedListener {
48 | public static final String TAG = "IapGooglePlay";
49 |
50 | private Map products = new HashMap();
51 | private BillingClient billingClient;
52 | private IPurchaseListener purchaseListener;
53 | private boolean autoFinishTransactions;
54 | private Activity activity;
55 |
56 | public IapGooglePlay(Activity activity, boolean autoFinishTransactions) {
57 | this.activity = activity;
58 | this.autoFinishTransactions = autoFinishTransactions;
59 |
60 | PendingPurchasesParams pendingPurchasesParams = PendingPurchasesParams.newBuilder().enableOneTimeProducts().build();
61 | billingClient = BillingClient.newBuilder(activity).setListener(this).enablePendingPurchases(pendingPurchasesParams).build();
62 | billingClient.startConnection(new BillingClientStateListener() {
63 | @Override
64 | public void onBillingSetupFinished(BillingResult billingResult) {
65 | if (billingResult.getResponseCode() == BillingResponseCode.OK) {
66 | Log.v(TAG, "Setup finished");
67 | // NOTE: we will not query purchases here. This is done
68 | // when the extension listener is set
69 | }
70 | else {
71 | Log.wtf(TAG, "Setup error: " + billingResult.getDebugMessage());
72 | }
73 | }
74 |
75 | @Override
76 | public void onBillingServiceDisconnected() {
77 | Log.v(TAG, "Service disconnected");
78 | }
79 | });
80 | }
81 |
82 | public void stop() {
83 | Log.d(TAG, "stop()");
84 | if (billingClient.isReady()) {
85 | billingClient.endConnection();
86 | }
87 | }
88 |
89 | private String toISO8601(final Date date) {
90 | String formatted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(date);
91 | return formatted.substring(0, 22) + ":" + formatted.substring(22);
92 | }
93 |
94 | private String convertPurchase(Purchase purchase) {
95 | // original JSON:
96 | // {
97 | // "orderId":"GPA.3301-1670-7033-37542",
98 | // "packageName":"com.defold.extension.iap",
99 | // "productId":"com.defold.iap.goldbar.medium",
100 | // "purchaseTime":1595455967875,
101 | // "purchaseState":0,
102 | // "purchaseToken":"kacckamkehbbammphdcnhbme.AO-J1OxznnK6E8ILqaAgrPa-3sfaHny424R1e_ZJ2LkaJVsy-5aEOmHizw0vUp-017m8OUvw1rSvfAHbOog1fIvDGJmjaze3MEVFOh1ayJsNFfPDUGwMA_u_9rlV7OqX_nnIyDShH2KE5WrnMC0yQyw7sg5hfgeW6A",
103 | // "acknowledged":false
104 | // }
105 | Log.d(TAG, "convertPurchase() original json: " + purchase.getOriginalJson());
106 | JSONObject p = new JSONObject();
107 | try {
108 | JSONObject original = new JSONObject(purchase.getOriginalJson());
109 | p.put("ident", original.get("productId"));
110 | p.put("state", purchaseStateToDefoldState(purchase.getPurchaseState()));
111 | p.put("trans_ident", purchase.getOrderId());
112 | p.put("date", toISO8601(new Date(purchase.getPurchaseTime())));
113 | p.put("receipt", purchase.getPurchaseToken());
114 | p.put("signature", purchase.getSignature());
115 | p.put("original_json", purchase.getOriginalJson());
116 | }
117 | catch (JSONException e) {
118 | Log.wtf(TAG, "Failed to convert purchase", e);
119 | }
120 | return p.toString();
121 | }
122 |
123 | private JSONArray convertSubscriptionOfferPricingPhases(SubscriptionOfferDetails details) {
124 | JSONArray a = new JSONArray();
125 | try {
126 | List pricingPhases = details.getPricingPhases().getPricingPhaseList();
127 | for (PricingPhase pricingPhase : pricingPhases) {
128 | JSONObject o = new JSONObject();
129 | o.put("price_string", pricingPhase.getFormattedPrice());
130 | o.put("price", pricingPhase.getPriceAmountMicros() * 0.000001);
131 | o.put("currency_code", pricingPhase.getPriceCurrencyCode());
132 | o.put("billing_period", pricingPhase.getBillingPeriod());
133 | o.put("billing_cycle_count", pricingPhase.getBillingCycleCount());
134 | switch (pricingPhase.getRecurrenceMode()) {
135 | case RecurrenceMode.FINITE_RECURRING:
136 | o.put("recurrence_mode", "FINITE");
137 | break;
138 | case RecurrenceMode.INFINITE_RECURRING:
139 | o.put("recurrence_mode", "INFINITE");
140 | break;
141 | default:
142 | case RecurrenceMode.NON_RECURRING:
143 | o.put("recurrence_mode", "NONE");
144 | break;
145 | }
146 | a.put(o);
147 | }
148 | }
149 | catch(JSONException e) {
150 | Log.wtf(TAG, "Failed to convert subscription offer pricing phases", e);
151 | }
152 | return a;
153 | }
154 |
155 | private JSONObject convertProductDetails(ProductDetails productDetails) {
156 | JSONObject p = new JSONObject();
157 | try {
158 | p.put("ident", productDetails.getProductId());
159 | p.put("title", productDetails.getTitle());
160 | p.put("description", productDetails.getDescription());
161 | if (productDetails.getProductType().equals(ProductType.INAPP)) {
162 | OneTimePurchaseOfferDetails offerDetails = productDetails.getOneTimePurchaseOfferDetails();
163 | p.put("price_string", offerDetails.getFormattedPrice());
164 | p.put("currency_code", offerDetails.getPriceCurrencyCode());
165 | p.put("price", offerDetails.getPriceAmountMicros() * 0.000001);
166 | }
167 | else if (productDetails.getProductType().equals(ProductType.SUBS)) {
168 | List subscriptionOfferDetails = productDetails.getSubscriptionOfferDetails();
169 | JSONArray a = new JSONArray();
170 | for (SubscriptionOfferDetails offerDetails : subscriptionOfferDetails) {
171 | JSONObject o = new JSONObject();
172 | o.put("token", offerDetails.getOfferToken());
173 | o.put("pricing", convertSubscriptionOfferPricingPhases(offerDetails));
174 | a.put(o);
175 | }
176 | p.put("subscriptions", a);
177 | }
178 | else {
179 | Log.i(TAG, "convertProductDetails() unknown product type " + productDetails.getProductType());
180 | }
181 | }
182 | catch(JSONException e) {
183 | Log.wtf(TAG, "Failed to convert product details", e);
184 | }
185 | return p;
186 | }
187 |
188 | private int purchaseStateToDefoldState(int purchaseState) {
189 | int defoldState;
190 | switch(purchaseState) {
191 | case PurchaseState.PENDING:
192 | defoldState = IapJNI.TRANS_STATE_PURCHASING;
193 | break;
194 | case PurchaseState.PURCHASED:
195 | defoldState = IapJNI.TRANS_STATE_PURCHASED;
196 | break;
197 | default:
198 | case PurchaseState.UNSPECIFIED_STATE:
199 | defoldState = IapJNI.TRANS_STATE_UNVERIFIED;
200 | break;
201 | }
202 | return defoldState;
203 | }
204 |
205 | private int billingResponseCodeToDefoldResponse(int responseCode) {
206 | int defoldResponse;
207 | switch(responseCode) {
208 | case BillingResponseCode.BILLING_UNAVAILABLE:
209 | defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE;
210 | break;
211 | case BillingResponseCode.DEVELOPER_ERROR:
212 | defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_DEVELOPER_ERROR;
213 | break;
214 | case BillingResponseCode.ITEM_ALREADY_OWNED:
215 | defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED;
216 | break;
217 | case BillingResponseCode.ITEM_NOT_OWNED:
218 | defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED;
219 | break;
220 | case BillingResponseCode.ITEM_UNAVAILABLE:
221 | defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE;
222 | break;
223 | case BillingResponseCode.OK:
224 | defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_OK;
225 | break;
226 | case BillingResponseCode.SERVICE_UNAVAILABLE:
227 | case BillingResponseCode.SERVICE_DISCONNECTED:
228 | defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE;
229 | break;
230 | case BillingResponseCode.USER_CANCELED:
231 | defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_USER_CANCELED;
232 | break;
233 | case BillingResponseCode.NETWORK_ERROR: // new in Play Billing Library 6.0.0
234 | defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_NETWORK_ERROR;
235 | break;
236 | case BillingResponseCode.FEATURE_NOT_SUPPORTED:
237 | case BillingResponseCode.ERROR:
238 | default:
239 | defoldResponse = IapJNI.BILLING_RESPONSE_RESULT_ERROR;
240 | break;
241 | }
242 | Log.d(TAG, "billingResponseCodeToDefoldResponse: " + responseCode + " defoldResponse: " + defoldResponse);
243 | return defoldResponse;
244 | }
245 |
246 | private int billingResultToDefoldResponse(BillingResult result) {
247 | return billingResponseCodeToDefoldResponse(result.getResponseCode());
248 | }
249 |
250 | private void invokeOnPurchaseResultListener(IPurchaseListener purchaseListener, int billingResultCode, String purchaseData) {
251 | if (purchaseListener == null) {
252 | Log.w(TAG, "Received billing result but no listener has been set");
253 | return;
254 | }
255 | purchaseListener.onPurchaseResult(billingResultCode, purchaseData);
256 | }
257 | private void invokeOnPurchaseResultListener(IPurchaseListener purchaseListener, BillingResult billingResult, Purchase purchase) {
258 | invokeOnPurchaseResultListener(purchaseListener, billingResultToDefoldResponse(billingResult), convertPurchase(purchase));
259 | }
260 | private void invokeOnPurchaseResultListener(IPurchaseListener purchaseListener, BillingResult billingResult) {
261 | invokeOnPurchaseResultListener(purchaseListener, billingResultToDefoldResponse(billingResult), "");
262 | }
263 |
264 | /**
265 | * This method is called either explicitly from Lua or from extension code
266 | * when "set_listener()" is called from Lua.
267 | * The method will query purchases and try to handle them one by one (either
268 | * trying to consume/finish them or simply notify the provided listener).
269 | */
270 | public void processPendingConsumables(final IPurchaseListener purchaseListener) {
271 | Log.d(TAG, "processPendingConsumables()");
272 |
273 |
274 | PurchasesResponseListener purchasesListener = new PurchasesResponseListener() {
275 | private List allPurchases = new ArrayList();
276 | private int queries = 2;
277 |
278 | @Override
279 | public void onQueryPurchasesResponse(BillingResult billingResult, List purchases) {
280 | if (billingResult.getResponseCode() != BillingResponseCode.OK) {
281 | Log.e(TAG, "Unable to query pending purchases: " + billingResult.getDebugMessage());
282 | }
283 | if (purchases != null) {
284 | allPurchases.addAll(purchases);
285 | }
286 | // we're finished when we have queried for both in-app and subs
287 | queries--;
288 | if (queries == 0) {
289 | for (Purchase purchase : allPurchases) {
290 | handlePurchase(purchase, purchaseListener);
291 | }
292 | }
293 | }
294 | };
295 |
296 | final QueryPurchasesParams inappParams = QueryPurchasesParams.newBuilder().setProductType(ProductType.INAPP).build();
297 | final QueryPurchasesParams subsParams = QueryPurchasesParams.newBuilder().setProductType(ProductType.SUBS).build();
298 | billingClient.queryPurchasesAsync(inappParams, purchasesListener);
299 | billingClient.queryPurchasesAsync(subsParams, purchasesListener);
300 | }
301 |
302 | /**
303 | * Consume a purchase. This will acknowledge the purchase and make it
304 | * available to buy again.
305 | */
306 | private void consumePurchase(final String purchaseToken, final ConsumeResponseListener consumeListener) {
307 | Log.d(TAG, "consumePurchase() " + purchaseToken);
308 | final ConsumeParams consumeParams = ConsumeParams.newBuilder()
309 | .setPurchaseToken(purchaseToken)
310 | .build();
311 |
312 | billingClient.consumeAsync(consumeParams, consumeListener);
313 | }
314 |
315 | /**
316 | * Called from Lua. This method will try to consume a purchase.
317 | */
318 | public void finishTransaction(final String purchaseToken, final IPurchaseListener purchaseListener) {
319 | Log.d(TAG, "finishTransaction() " + purchaseToken);
320 | consumePurchase(purchaseToken, new ConsumeResponseListener() {
321 | @Override
322 | public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
323 | Log.d(TAG, "finishTransaction() response code " + billingResult.getResponseCode() + " purchaseToken: " + purchaseToken);
324 | // note: we only call the purchase listener if an error happens
325 | if (billingResult.getResponseCode() != BillingResponseCode.OK) {
326 | Log.e(TAG, "Unable to consume purchase: " + billingResult.getDebugMessage());
327 | invokeOnPurchaseResultListener(purchaseListener, billingResult);
328 | }
329 | }
330 | });
331 | }
332 |
333 | /**
334 | * Called from Lua. This method will try to acknowledge a purchase (but not finish/consume it).
335 | */
336 | public void acknowledgeTransaction(final String purchaseToken, final IPurchaseListener purchaseListener) {
337 | Log.d(TAG, "acknowledgeTransaction() " + purchaseToken);
338 |
339 | AcknowledgePurchaseParams acknowledgeParams = AcknowledgePurchaseParams.newBuilder()
340 | .setPurchaseToken(purchaseToken)
341 | .build();
342 |
343 | billingClient.acknowledgePurchase(acknowledgeParams, new AcknowledgePurchaseResponseListener() {
344 | @Override
345 | public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
346 | Log.d(TAG, "acknowledgeTransaction() response code " + billingResult.getResponseCode());
347 | // note: we only call the purchase listener if an error happens
348 | if (billingResult.getResponseCode() != BillingResponseCode.OK) {
349 | Log.e(TAG, "Unable to acknowledge purchase: " + billingResult.getDebugMessage());
350 | invokeOnPurchaseResultListener(purchaseListener, billingResult);
351 | }
352 | }
353 | });
354 | }
355 |
356 | /**
357 | * Handle a purchase. If the extension is configured to automatically
358 | * finish transactions the purchase will be immediately consumed. Otherwise
359 | * the product will be returned via the listener without being consumed.
360 | * NOTE: Billing 3.0+ requires purchases to be acknowledged within 3 days of
361 | * purchase unless they are consumed.
362 | */
363 | private void handlePurchase(final Purchase purchase, final IPurchaseListener purchaseListener) {
364 | if (this.autoFinishTransactions) {
365 | consumePurchase(purchase.getPurchaseToken(), new ConsumeResponseListener() {
366 | @Override
367 | public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
368 | Log.d(TAG, "handlePurchase() response code " + billingResult.getResponseCode() + " purchaseToken: " + purchaseToken);
369 | invokeOnPurchaseResultListener(purchaseListener, billingResult, purchase);
370 | }
371 | });
372 | }
373 | else {
374 | invokeOnPurchaseResultListener(purchaseListener, billingResponseCodeToDefoldResponse(BillingResponseCode.OK), convertPurchase(purchase));
375 | }
376 | }
377 |
378 | /**
379 | * BillingClient listener set in the constructor.
380 | */
381 | @Override
382 | public void onPurchasesUpdated(BillingResult billingResult, List purchases) {
383 | if (billingResult.getResponseCode() == BillingResponseCode.OK) {
384 | if (purchases != null && !purchases.isEmpty()) {
385 | for (Purchase purchase : purchases) {
386 | if (purchase != null) {
387 | handlePurchase(purchase, this.purchaseListener);
388 | }
389 | }
390 | }
391 | }
392 | else {
393 | invokeOnPurchaseResultListener(this.purchaseListener, billingResult);
394 | }
395 | }
396 |
397 | /**
398 | * Buy a product. This method stores the listener and uses it in the
399 | * onPurchasesUpdated() callback.
400 | */
401 | private void buyProduct(ProductDetails pd, final String token, final IPurchaseListener purchaseListener) {
402 | this.purchaseListener = purchaseListener;
403 |
404 | List productDetailsParams = new ArrayList();
405 | if (pd.getProductType().equals(ProductType.SUBS)) {
406 | productDetailsParams.add(ProductDetailsParams.newBuilder().setProductDetails(pd).setOfferToken(token).build());
407 | }
408 | else {
409 | productDetailsParams.add(ProductDetailsParams.newBuilder().setProductDetails(pd).build());
410 | }
411 |
412 | final BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder().setProductDetailsParamsList(productDetailsParams).build();
413 |
414 | BillingResult billingResult = billingClient.launchBillingFlow(this.activity, billingFlowParams);
415 | if (billingResult.getResponseCode() != BillingResponseCode.OK) {
416 | Log.e(TAG, "Purchase failed: " + billingResult.getDebugMessage());
417 | invokeOnPurchaseResultListener(purchaseListener, billingResult);
418 | }
419 | }
420 |
421 | /**
422 | * Called from Lua.
423 | */
424 | public void buy(final String product, final String token, final IPurchaseListener purchaseListener) {
425 | Log.d(TAG, "buy()");
426 |
427 | ProductDetails pd = this.products.get(product);
428 | if (pd != null) {
429 | buyProduct(pd, token, purchaseListener);
430 | }
431 | else {
432 | List productList = new ArrayList();
433 | productList.add(product);
434 | queryProductDetailsAsync(productList, new ProductDetailsResponseListener() {
435 | @Override
436 | public void onProductDetailsResponse(BillingResult billingResult, List productDetailsList) {
437 | if (billingResult.getResponseCode() == BillingResponseCode.OK && (productDetailsList != null) && !productDetailsList.isEmpty()) {
438 | for (ProductDetails productDetails : productDetailsList) {
439 | if (productDetails != null) {
440 | buyProduct(productDetails, token, purchaseListener);
441 | break;
442 | }
443 | }
444 | }
445 | else {
446 | Log.e(TAG, "Unable to get product details before buying: " + billingResult.getDebugMessage());
447 | invokeOnPurchaseResultListener(purchaseListener, billingResult);
448 | }
449 | }
450 | });
451 | }
452 | }
453 |
454 | /**
455 | * Get details for a list of products. The products can be a mix of
456 | * in-app products and subscriptions.
457 | */
458 | private void queryProductDetailsAsync(final List productList, final ProductDetailsResponseListener listener) {
459 | ProductDetailsResponseListener detailsListener = new ProductDetailsResponseListener() {
460 | private List allProductDetails = new ArrayList();
461 | private int queries = 2;
462 |
463 | @Override
464 | public void onProductDetailsResponse(BillingResult billingResult, List productDetails) {
465 | if (productDetails != null && !productDetails.isEmpty()) {
466 | // cache products (cache will be used to speed up buying)
467 | for (ProductDetails pd : productDetails) {
468 | if (pd != null) {
469 | IapGooglePlay.this.products.put(pd.getProductId(), pd);
470 | }
471 | }
472 | // add to list of all product details
473 | allProductDetails.addAll(productDetails);
474 | }
475 | // we're finished when we have queried for both in-app and subs
476 | queries--;
477 | if (queries == 0) {
478 | listener.onProductDetailsResponse(billingResult, allProductDetails);
479 | }
480 | }
481 | };
482 |
483 | // we don't know if a product is a subscription or inapp product type
484 | // instread we create two product lists from the same set of products and use INAPP for one and SUBS for the other
485 | List inappProductList = new ArrayList();
486 | List subsProductList = new ArrayList();
487 | for (String productId : productList) {
488 | inappProductList.add(Product.newBuilder().setProductId(productId).setProductType(ProductType.INAPP).build());
489 | subsProductList.add(Product.newBuilder().setProductId(productId).setProductType(ProductType.SUBS).build());
490 | }
491 |
492 | // do one query per product type
493 | final QueryProductDetailsParams inappParams = QueryProductDetailsParams.newBuilder().setProductList(inappProductList).build();
494 | final QueryProductDetailsParams subsParams = QueryProductDetailsParams.newBuilder().setProductList(subsProductList).build();
495 | billingClient.queryProductDetailsAsync(inappParams, detailsListener);
496 | billingClient.queryProductDetailsAsync(subsParams, detailsListener);
497 | }
498 |
499 | /**
500 | * Called from Lua.
501 | */
502 | public void listItems(final String products, final IListProductsListener productsListener, final long commandPtr) {
503 | Log.d(TAG, "listItems()");
504 |
505 | // create list of product ids from comma separated string
506 | List productList = new ArrayList();
507 | for (String p : products.split(",")) {
508 | if (p.trim().length() > 0) {
509 | productList.add(p);
510 | }
511 | }
512 |
513 | queryProductDetailsAsync(productList, new ProductDetailsResponseListener() {
514 | @Override
515 | public void onProductDetailsResponse(BillingResult billingResult, List productDetails) {
516 | JSONArray a = new JSONArray();
517 | if ((billingResult.getResponseCode() == BillingResponseCode.OK) && (productDetails != null) && !productDetails.isEmpty()) {
518 | for (ProductDetails pd : productDetails) {
519 | if (pd != null) {
520 | a.put(convertProductDetails(pd));
521 | }
522 | }
523 | }
524 | else {
525 | Log.e(TAG, "Unable to list products: " + billingResult.getDebugMessage());
526 | }
527 | productsListener.onProductsResult(billingResultToDefoldResponse(billingResult), a.toString(), commandPtr);
528 | }
529 | });
530 | }
531 |
532 | /**
533 | * Called from Lua.
534 | */
535 | public void restore(final IPurchaseListener listener) {
536 | Log.d(TAG, "restore()");
537 | processPendingConsumables(listener);
538 | }
539 | }
540 |
--------------------------------------------------------------------------------
/extension-iap/src/java/com/defold/iap/IapJNI.java:
--------------------------------------------------------------------------------
1 | package com.defold.iap;
2 |
3 | public class IapJNI implements IListProductsListener, IPurchaseListener {
4 |
5 | // NOTE: Also defined in iap.h
6 | public static final int TRANS_STATE_PURCHASING = 0;
7 | public static final int TRANS_STATE_PURCHASED = 1;
8 | public static final int TRANS_STATE_FAILED = 2;
9 | public static final int TRANS_STATE_RESTORED = 3;
10 | public static final int TRANS_STATE_UNVERIFIED = 4;
11 |
12 | public static final int BILLING_RESPONSE_RESULT_OK = 0;
13 | public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
14 | public static final int BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE = 2;
15 | public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
16 | public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
17 | public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
18 | public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
19 | public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
20 | public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
21 | public static final int BILLING_RESPONSE_RESULT_NETWORK_ERROR = 9;
22 |
23 | public IapJNI() {
24 | }
25 |
26 | @Override
27 | public native void onProductsResult(int responseCode, String productList, long cmdHandle);
28 |
29 | @Override
30 | public native void onPurchaseResult(int responseCode, String purchaseData);
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/game.project:
--------------------------------------------------------------------------------
1 | [bootstrap]
2 | main_collection = /main/main.collectionc
3 |
4 | [script]
5 | shared_state = 1
6 |
7 | [display]
8 | width = 640
9 | height = 1136
10 |
11 | [android]
12 | input_method = HiddenInputField
13 | package = com.defold.extension.iap
14 | version_code = 9
15 | minimum_sdk_version = 21
16 |
17 | [project]
18 | title = extension-iap
19 | dependencies#0 = https://github.com/andsve/dirtylarry/archive/master.zip
20 |
21 | [library]
22 | include_dirs = extension-iap
23 |
24 | [ios]
25 | bundle_identifier = com.defold.extension.iap
26 |
27 | [iap]
28 | auto_finish_transactions = 0
29 |
30 | [osx]
31 | bundle_identifier = com.defold.extension.iap
32 |
33 |
--------------------------------------------------------------------------------
/input/game.input_binding:
--------------------------------------------------------------------------------
1 | mouse_trigger {
2 | input: MOUSE_BUTTON_1
3 | action: "touch"
4 | }
5 |
--------------------------------------------------------------------------------
/main/main.collection:
--------------------------------------------------------------------------------
1 | name: "main"
2 | scale_along_z: 0
3 | embedded_instances {
4 | id: "go"
5 | data: "components {\n"
6 | " id: \"main\"\n"
7 | " component: \"/main/main.gui\"\n"
8 | " position {\n"
9 | " x: 0.0\n"
10 | " y: 0.0\n"
11 | " z: 0.0\n"
12 | " }\n"
13 | " rotation {\n"
14 | " x: 0.0\n"
15 | " y: 0.0\n"
16 | " z: 0.0\n"
17 | " w: 1.0\n"
18 | " }\n"
19 | "}\n"
20 | ""
21 | position {
22 | x: 0.0
23 | y: 0.0
24 | z: 0.0
25 | }
26 | rotation {
27 | x: 0.0
28 | y: 0.0
29 | z: 0.0
30 | w: 1.0
31 | }
32 | scale3 {
33 | x: 1.0
34 | y: 1.0
35 | z: 1.0
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/main/main.gui:
--------------------------------------------------------------------------------
1 | script: "/main/main.gui_script"
2 | fonts {
3 | name: "default"
4 | font: "/builtins/fonts/default.font"
5 | }
6 | background_color {
7 | x: 0.0
8 | y: 0.0
9 | z: 0.0
10 | w: 0.0
11 | }
12 | nodes {
13 | position {
14 | x: 171.0
15 | y: 569.0
16 | z: 0.0
17 | w: 1.0
18 | }
19 | rotation {
20 | x: 0.0
21 | y: 0.0
22 | z: 0.0
23 | w: 1.0
24 | }
25 | scale {
26 | x: 1.0
27 | y: 1.0
28 | z: 1.0
29 | w: 1.0
30 | }
31 | size {
32 | x: 200.0
33 | y: 100.0
34 | z: 0.0
35 | w: 1.0
36 | }
37 | color {
38 | x: 1.0
39 | y: 1.0
40 | z: 1.0
41 | w: 1.0
42 | }
43 | type: TYPE_TEMPLATE
44 | id: "goldbars_small"
45 | layer: ""
46 | inherit_alpha: true
47 | alpha: 1.0
48 | template: "/dirtylarry/button.gui"
49 | template_node_child: false
50 | custom_type: 0
51 | enabled: true
52 | }
53 | nodes {
54 | position {
55 | x: 0.0
56 | y: 0.0
57 | z: 0.0
58 | w: 1.0
59 | }
60 | rotation {
61 | x: 0.0
62 | y: 0.0
63 | z: 0.0
64 | w: 1.0
65 | }
66 | scale {
67 | x: 1.0
68 | y: 1.0
69 | z: 1.0
70 | w: 1.0
71 | }
72 | size {
73 | x: 300.0
74 | y: 88.0
75 | z: 0.0
76 | w: 1.0
77 | }
78 | color {
79 | x: 1.0
80 | y: 1.0
81 | z: 1.0
82 | w: 1.0
83 | }
84 | type: TYPE_BOX
85 | blend_mode: BLEND_MODE_ALPHA
86 | texture: "button/button_normal"
87 | id: "goldbars_small/larrybutton"
88 | xanchor: XANCHOR_NONE
89 | yanchor: YANCHOR_NONE
90 | pivot: PIVOT_CENTER
91 | adjust_mode: ADJUST_MODE_FIT
92 | parent: "goldbars_small"
93 | layer: ""
94 | inherit_alpha: true
95 | slice9 {
96 | x: 32.0
97 | y: 32.0
98 | z: 32.0
99 | w: 32.0
100 | }
101 | clipping_mode: CLIPPING_MODE_NONE
102 | clipping_visible: true
103 | clipping_inverted: false
104 | alpha: 1.0
105 | template_node_child: true
106 | size_mode: SIZE_MODE_MANUAL
107 | custom_type: 0
108 | enabled: true
109 | visible: true
110 | material: ""
111 | }
112 | nodes {
113 | position {
114 | x: 0.0
115 | y: 0.0
116 | z: 0.0
117 | w: 1.0
118 | }
119 | rotation {
120 | x: 0.0
121 | y: 0.0
122 | z: 0.0
123 | w: 1.0
124 | }
125 | scale {
126 | x: 1.0
127 | y: 1.0
128 | z: 1.0
129 | w: 1.0
130 | }
131 | size {
132 | x: 200.0
133 | y: 100.0
134 | z: 0.0
135 | w: 1.0
136 | }
137 | color {
138 | x: 1.0
139 | y: 1.0
140 | z: 1.0
141 | w: 1.0
142 | }
143 | type: TYPE_TEXT
144 | blend_mode: BLEND_MODE_ALPHA
145 | text: "Goldbars - S"
146 | font: "larryfont"
147 | id: "goldbars_small/larrylabel"
148 | xanchor: XANCHOR_NONE
149 | yanchor: YANCHOR_NONE
150 | pivot: PIVOT_CENTER
151 | outline {
152 | x: 0.0
153 | y: 0.0
154 | z: 0.0
155 | w: 1.0
156 | }
157 | shadow {
158 | x: 1.0
159 | y: 1.0
160 | z: 1.0
161 | w: 1.0
162 | }
163 | adjust_mode: ADJUST_MODE_FIT
164 | line_break: false
165 | parent: "goldbars_small/larrybutton"
166 | layer: ""
167 | inherit_alpha: true
168 | alpha: 1.0
169 | outline_alpha: 1.0
170 | shadow_alpha: 1.0
171 | overridden_fields: 8
172 | template_node_child: true
173 | text_leading: 1.0
174 | text_tracking: 0.0
175 | custom_type: 0
176 | enabled: true
177 | visible: true
178 | material: ""
179 | }
180 | nodes {
181 | position {
182 | x: 530.0
183 | y: 56.0
184 | z: 0.0
185 | w: 1.0
186 | }
187 | rotation {
188 | x: 0.0
189 | y: 0.0
190 | z: 0.0
191 | w: 1.0
192 | }
193 | scale {
194 | x: 1.0
195 | y: 1.0
196 | z: 1.0
197 | w: 1.0
198 | }
199 | size {
200 | x: 200.0
201 | y: 100.0
202 | z: 0.0
203 | w: 1.0
204 | }
205 | color {
206 | x: 1.0
207 | y: 1.0
208 | z: 1.0
209 | w: 1.0
210 | }
211 | type: TYPE_TEMPLATE
212 | id: "restore"
213 | layer: ""
214 | inherit_alpha: true
215 | alpha: 1.0
216 | template: "/dirtylarry/button.gui"
217 | template_node_child: false
218 | custom_type: 0
219 | enabled: true
220 | }
221 | nodes {
222 | position {
223 | x: 0.0
224 | y: 0.0
225 | z: 0.0
226 | w: 1.0
227 | }
228 | rotation {
229 | x: 0.0
230 | y: 0.0
231 | z: 0.0
232 | w: 1.0
233 | }
234 | scale {
235 | x: 1.0
236 | y: 1.0
237 | z: 1.0
238 | w: 1.0
239 | }
240 | size {
241 | x: 200.0
242 | y: 88.0
243 | z: 0.0
244 | w: 1.0
245 | }
246 | color {
247 | x: 1.0
248 | y: 1.0
249 | z: 1.0
250 | w: 1.0
251 | }
252 | type: TYPE_BOX
253 | blend_mode: BLEND_MODE_ALPHA
254 | texture: "button/button_normal"
255 | id: "restore/larrybutton"
256 | xanchor: XANCHOR_NONE
257 | yanchor: YANCHOR_NONE
258 | pivot: PIVOT_CENTER
259 | adjust_mode: ADJUST_MODE_FIT
260 | parent: "restore"
261 | layer: ""
262 | inherit_alpha: true
263 | slice9 {
264 | x: 32.0
265 | y: 32.0
266 | z: 32.0
267 | w: 32.0
268 | }
269 | clipping_mode: CLIPPING_MODE_NONE
270 | clipping_visible: true
271 | clipping_inverted: false
272 | alpha: 1.0
273 | overridden_fields: 4
274 | template_node_child: true
275 | size_mode: SIZE_MODE_MANUAL
276 | custom_type: 0
277 | enabled: true
278 | visible: true
279 | material: ""
280 | }
281 | nodes {
282 | position {
283 | x: 0.0
284 | y: 0.0
285 | z: 0.0
286 | w: 1.0
287 | }
288 | rotation {
289 | x: 0.0
290 | y: 0.0
291 | z: 0.0
292 | w: 1.0
293 | }
294 | scale {
295 | x: 1.0
296 | y: 1.0
297 | z: 1.0
298 | w: 1.0
299 | }
300 | size {
301 | x: 200.0
302 | y: 100.0
303 | z: 0.0
304 | w: 1.0
305 | }
306 | color {
307 | x: 1.0
308 | y: 1.0
309 | z: 1.0
310 | w: 1.0
311 | }
312 | type: TYPE_TEXT
313 | blend_mode: BLEND_MODE_ALPHA
314 | text: "Restore"
315 | font: "larryfont"
316 | id: "restore/larrylabel"
317 | xanchor: XANCHOR_NONE
318 | yanchor: YANCHOR_NONE
319 | pivot: PIVOT_CENTER
320 | outline {
321 | x: 0.0
322 | y: 0.0
323 | z: 0.0
324 | w: 1.0
325 | }
326 | shadow {
327 | x: 1.0
328 | y: 1.0
329 | z: 1.0
330 | w: 1.0
331 | }
332 | adjust_mode: ADJUST_MODE_FIT
333 | line_break: false
334 | parent: "restore/larrybutton"
335 | layer: ""
336 | inherit_alpha: true
337 | alpha: 1.0
338 | outline_alpha: 1.0
339 | shadow_alpha: 1.0
340 | overridden_fields: 8
341 | template_node_child: true
342 | text_leading: 1.0
343 | text_tracking: 0.0
344 | custom_type: 0
345 | enabled: true
346 | visible: true
347 | material: ""
348 | }
349 | nodes {
350 | position {
351 | x: 20.0
352 | y: 1124.0
353 | z: 0.0
354 | w: 1.0
355 | }
356 | rotation {
357 | x: 0.0
358 | y: 0.0
359 | z: 0.0
360 | w: 1.0
361 | }
362 | scale {
363 | x: 1.0
364 | y: 1.0
365 | z: 1.0
366 | w: 1.0
367 | }
368 | size {
369 | x: 600.0
370 | y: 450.0
371 | z: 0.0
372 | w: 1.0
373 | }
374 | color {
375 | x: 1.0
376 | y: 1.0
377 | z: 1.0
378 | w: 1.0
379 | }
380 | type: TYPE_TEXT
381 | blend_mode: BLEND_MODE_ALPHA
382 | text: ""
383 | font: "default"
384 | id: "log"
385 | xanchor: XANCHOR_NONE
386 | yanchor: YANCHOR_NONE
387 | pivot: PIVOT_NW
388 | outline {
389 | x: 1.0
390 | y: 1.0
391 | z: 1.0
392 | w: 1.0
393 | }
394 | shadow {
395 | x: 1.0
396 | y: 1.0
397 | z: 1.0
398 | w: 1.0
399 | }
400 | adjust_mode: ADJUST_MODE_FIT
401 | line_break: true
402 | layer: ""
403 | inherit_alpha: true
404 | alpha: 1.0
405 | outline_alpha: 1.0
406 | shadow_alpha: 1.0
407 | template_node_child: false
408 | text_leading: 1.0
409 | text_tracking: 0.0
410 | custom_type: 0
411 | enabled: true
412 | visible: true
413 | material: ""
414 | }
415 | nodes {
416 | position {
417 | x: 171.0
418 | y: 465.0
419 | z: 0.0
420 | w: 1.0
421 | }
422 | rotation {
423 | x: 0.0
424 | y: 0.0
425 | z: 0.0
426 | w: 1.0
427 | }
428 | scale {
429 | x: 1.0
430 | y: 1.0
431 | z: 1.0
432 | w: 1.0
433 | }
434 | size {
435 | x: 200.0
436 | y: 100.0
437 | z: 0.0
438 | w: 1.0
439 | }
440 | color {
441 | x: 1.0
442 | y: 1.0
443 | z: 1.0
444 | w: 1.0
445 | }
446 | type: TYPE_TEMPLATE
447 | id: "goldbars_medium"
448 | layer: ""
449 | inherit_alpha: true
450 | alpha: 1.0
451 | template: "/dirtylarry/button.gui"
452 | template_node_child: false
453 | custom_type: 0
454 | enabled: true
455 | }
456 | nodes {
457 | position {
458 | x: 0.0
459 | y: 0.0
460 | z: 0.0
461 | w: 1.0
462 | }
463 | rotation {
464 | x: 0.0
465 | y: 0.0
466 | z: 0.0
467 | w: 1.0
468 | }
469 | scale {
470 | x: 1.0
471 | y: 1.0
472 | z: 1.0
473 | w: 1.0
474 | }
475 | size {
476 | x: 300.0
477 | y: 88.0
478 | z: 0.0
479 | w: 1.0
480 | }
481 | color {
482 | x: 1.0
483 | y: 1.0
484 | z: 1.0
485 | w: 1.0
486 | }
487 | type: TYPE_BOX
488 | blend_mode: BLEND_MODE_ALPHA
489 | texture: "button/button_normal"
490 | id: "goldbars_medium/larrybutton"
491 | xanchor: XANCHOR_NONE
492 | yanchor: YANCHOR_NONE
493 | pivot: PIVOT_CENTER
494 | adjust_mode: ADJUST_MODE_FIT
495 | parent: "goldbars_medium"
496 | layer: ""
497 | inherit_alpha: true
498 | slice9 {
499 | x: 32.0
500 | y: 32.0
501 | z: 32.0
502 | w: 32.0
503 | }
504 | clipping_mode: CLIPPING_MODE_NONE
505 | clipping_visible: true
506 | clipping_inverted: false
507 | alpha: 1.0
508 | template_node_child: true
509 | size_mode: SIZE_MODE_MANUAL
510 | custom_type: 0
511 | enabled: true
512 | visible: true
513 | material: ""
514 | }
515 | nodes {
516 | position {
517 | x: 0.0
518 | y: 0.0
519 | z: 0.0
520 | w: 1.0
521 | }
522 | rotation {
523 | x: 0.0
524 | y: 0.0
525 | z: 0.0
526 | w: 1.0
527 | }
528 | scale {
529 | x: 1.0
530 | y: 1.0
531 | z: 1.0
532 | w: 1.0
533 | }
534 | size {
535 | x: 200.0
536 | y: 100.0
537 | z: 0.0
538 | w: 1.0
539 | }
540 | color {
541 | x: 1.0
542 | y: 1.0
543 | z: 1.0
544 | w: 1.0
545 | }
546 | type: TYPE_TEXT
547 | blend_mode: BLEND_MODE_ALPHA
548 | text: "Goldbars - M"
549 | font: "larryfont"
550 | id: "goldbars_medium/larrylabel"
551 | xanchor: XANCHOR_NONE
552 | yanchor: YANCHOR_NONE
553 | pivot: PIVOT_CENTER
554 | outline {
555 | x: 0.0
556 | y: 0.0
557 | z: 0.0
558 | w: 1.0
559 | }
560 | shadow {
561 | x: 1.0
562 | y: 1.0
563 | z: 1.0
564 | w: 1.0
565 | }
566 | adjust_mode: ADJUST_MODE_FIT
567 | line_break: false
568 | parent: "goldbars_medium/larrybutton"
569 | layer: ""
570 | inherit_alpha: true
571 | alpha: 1.0
572 | outline_alpha: 1.0
573 | shadow_alpha: 1.0
574 | overridden_fields: 8
575 | template_node_child: true
576 | text_leading: 1.0
577 | text_tracking: 0.0
578 | custom_type: 0
579 | enabled: true
580 | visible: true
581 | material: ""
582 | }
583 | nodes {
584 | position {
585 | x: 171.0
586 | y: 357.0
587 | z: 0.0
588 | w: 1.0
589 | }
590 | rotation {
591 | x: 0.0
592 | y: 0.0
593 | z: 0.0
594 | w: 1.0
595 | }
596 | scale {
597 | x: 1.0
598 | y: 1.0
599 | z: 1.0
600 | w: 1.0
601 | }
602 | size {
603 | x: 200.0
604 | y: 100.0
605 | z: 0.0
606 | w: 1.0
607 | }
608 | color {
609 | x: 1.0
610 | y: 1.0
611 | z: 1.0
612 | w: 1.0
613 | }
614 | type: TYPE_TEMPLATE
615 | id: "goldbars_large"
616 | layer: ""
617 | inherit_alpha: true
618 | alpha: 1.0
619 | template: "/dirtylarry/button.gui"
620 | template_node_child: false
621 | custom_type: 0
622 | enabled: true
623 | }
624 | nodes {
625 | position {
626 | x: 0.0
627 | y: 0.0
628 | z: 0.0
629 | w: 1.0
630 | }
631 | rotation {
632 | x: 0.0
633 | y: 0.0
634 | z: 0.0
635 | w: 1.0
636 | }
637 | scale {
638 | x: 1.0
639 | y: 1.0
640 | z: 1.0
641 | w: 1.0
642 | }
643 | size {
644 | x: 300.0
645 | y: 88.0
646 | z: 0.0
647 | w: 1.0
648 | }
649 | color {
650 | x: 1.0
651 | y: 1.0
652 | z: 1.0
653 | w: 1.0
654 | }
655 | type: TYPE_BOX
656 | blend_mode: BLEND_MODE_ALPHA
657 | texture: "button/button_normal"
658 | id: "goldbars_large/larrybutton"
659 | xanchor: XANCHOR_NONE
660 | yanchor: YANCHOR_NONE
661 | pivot: PIVOT_CENTER
662 | adjust_mode: ADJUST_MODE_FIT
663 | parent: "goldbars_large"
664 | layer: ""
665 | inherit_alpha: true
666 | slice9 {
667 | x: 32.0
668 | y: 32.0
669 | z: 32.0
670 | w: 32.0
671 | }
672 | clipping_mode: CLIPPING_MODE_NONE
673 | clipping_visible: true
674 | clipping_inverted: false
675 | alpha: 1.0
676 | template_node_child: true
677 | size_mode: SIZE_MODE_MANUAL
678 | custom_type: 0
679 | enabled: true
680 | visible: true
681 | material: ""
682 | }
683 | nodes {
684 | position {
685 | x: 0.0
686 | y: 0.0
687 | z: 0.0
688 | w: 1.0
689 | }
690 | rotation {
691 | x: 0.0
692 | y: 0.0
693 | z: 0.0
694 | w: 1.0
695 | }
696 | scale {
697 | x: 1.0
698 | y: 1.0
699 | z: 1.0
700 | w: 1.0
701 | }
702 | size {
703 | x: 200.0
704 | y: 100.0
705 | z: 0.0
706 | w: 1.0
707 | }
708 | color {
709 | x: 1.0
710 | y: 1.0
711 | z: 1.0
712 | w: 1.0
713 | }
714 | type: TYPE_TEXT
715 | blend_mode: BLEND_MODE_ALPHA
716 | text: "Goldbars - L"
717 | font: "larryfont"
718 | id: "goldbars_large/larrylabel"
719 | xanchor: XANCHOR_NONE
720 | yanchor: YANCHOR_NONE
721 | pivot: PIVOT_CENTER
722 | outline {
723 | x: 0.0
724 | y: 0.0
725 | z: 0.0
726 | w: 1.0
727 | }
728 | shadow {
729 | x: 1.0
730 | y: 1.0
731 | z: 1.0
732 | w: 1.0
733 | }
734 | adjust_mode: ADJUST_MODE_FIT
735 | line_break: false
736 | parent: "goldbars_large/larrybutton"
737 | layer: ""
738 | inherit_alpha: true
739 | alpha: 1.0
740 | outline_alpha: 1.0
741 | shadow_alpha: 1.0
742 | overridden_fields: 8
743 | template_node_child: true
744 | text_leading: 1.0
745 | text_tracking: 0.0
746 | custom_type: 0
747 | enabled: true
748 | visible: true
749 | material: ""
750 | }
751 | nodes {
752 | position {
753 | x: 109.0
754 | y: 56.0
755 | z: 0.0
756 | w: 1.0
757 | }
758 | rotation {
759 | x: 0.0
760 | y: 0.0
761 | z: 0.0
762 | w: 1.0
763 | }
764 | scale {
765 | x: 1.0
766 | y: 1.0
767 | z: 1.0
768 | w: 1.0
769 | }
770 | size {
771 | x: 200.0
772 | y: 100.0
773 | z: 0.0
774 | w: 1.0
775 | }
776 | color {
777 | x: 1.0
778 | y: 1.0
779 | z: 1.0
780 | w: 1.0
781 | }
782 | type: TYPE_TEMPLATE
783 | id: "list"
784 | layer: ""
785 | inherit_alpha: true
786 | alpha: 1.0
787 | template: "/dirtylarry/button.gui"
788 | template_node_child: false
789 | custom_type: 0
790 | enabled: true
791 | }
792 | nodes {
793 | position {
794 | x: 0.0
795 | y: 0.0
796 | z: 0.0
797 | w: 1.0
798 | }
799 | rotation {
800 | x: 0.0
801 | y: 0.0
802 | z: 0.0
803 | w: 1.0
804 | }
805 | scale {
806 | x: 1.0
807 | y: 1.0
808 | z: 1.0
809 | w: 1.0
810 | }
811 | size {
812 | x: 200.0
813 | y: 88.0
814 | z: 0.0
815 | w: 1.0
816 | }
817 | color {
818 | x: 1.0
819 | y: 1.0
820 | z: 1.0
821 | w: 1.0
822 | }
823 | type: TYPE_BOX
824 | blend_mode: BLEND_MODE_ALPHA
825 | texture: "button/button_normal"
826 | id: "list/larrybutton"
827 | xanchor: XANCHOR_NONE
828 | yanchor: YANCHOR_NONE
829 | pivot: PIVOT_CENTER
830 | adjust_mode: ADJUST_MODE_FIT
831 | parent: "list"
832 | layer: ""
833 | inherit_alpha: true
834 | slice9 {
835 | x: 32.0
836 | y: 32.0
837 | z: 32.0
838 | w: 32.0
839 | }
840 | clipping_mode: CLIPPING_MODE_NONE
841 | clipping_visible: true
842 | clipping_inverted: false
843 | alpha: 1.0
844 | overridden_fields: 4
845 | template_node_child: true
846 | size_mode: SIZE_MODE_MANUAL
847 | custom_type: 0
848 | enabled: true
849 | visible: true
850 | material: ""
851 | }
852 | nodes {
853 | position {
854 | x: 0.0
855 | y: 0.0
856 | z: 0.0
857 | w: 1.0
858 | }
859 | rotation {
860 | x: 0.0
861 | y: 0.0
862 | z: 0.0
863 | w: 1.0
864 | }
865 | scale {
866 | x: 1.0
867 | y: 1.0
868 | z: 1.0
869 | w: 1.0
870 | }
871 | size {
872 | x: 200.0
873 | y: 100.0
874 | z: 0.0
875 | w: 1.0
876 | }
877 | color {
878 | x: 1.0
879 | y: 1.0
880 | z: 1.0
881 | w: 1.0
882 | }
883 | type: TYPE_TEXT
884 | blend_mode: BLEND_MODE_ALPHA
885 | text: "List"
886 | font: "larryfont"
887 | id: "list/larrylabel"
888 | xanchor: XANCHOR_NONE
889 | yanchor: YANCHOR_NONE
890 | pivot: PIVOT_CENTER
891 | outline {
892 | x: 0.0
893 | y: 0.0
894 | z: 0.0
895 | w: 1.0
896 | }
897 | shadow {
898 | x: 1.0
899 | y: 1.0
900 | z: 1.0
901 | w: 1.0
902 | }
903 | adjust_mode: ADJUST_MODE_FIT
904 | line_break: false
905 | parent: "list/larrybutton"
906 | layer: ""
907 | inherit_alpha: true
908 | alpha: 1.0
909 | outline_alpha: 1.0
910 | shadow_alpha: 1.0
911 | overridden_fields: 8
912 | template_node_child: true
913 | text_leading: 1.0
914 | text_tracking: 0.0
915 | custom_type: 0
916 | enabled: true
917 | visible: true
918 | material: ""
919 | }
920 | nodes {
921 | position {
922 | x: 171.0
923 | y: 251.0
924 | z: 0.0
925 | w: 1.0
926 | }
927 | rotation {
928 | x: 0.0
929 | y: 0.0
930 | z: 0.0
931 | w: 1.0
932 | }
933 | scale {
934 | x: 1.0
935 | y: 1.0
936 | z: 1.0
937 | w: 1.0
938 | }
939 | size {
940 | x: 200.0
941 | y: 100.0
942 | z: 0.0
943 | w: 1.0
944 | }
945 | color {
946 | x: 1.0
947 | y: 1.0
948 | z: 1.0
949 | w: 1.0
950 | }
951 | type: TYPE_TEMPLATE
952 | id: "subscription"
953 | layer: ""
954 | inherit_alpha: true
955 | alpha: 1.0
956 | template: "/dirtylarry/button.gui"
957 | template_node_child: false
958 | custom_type: 0
959 | enabled: true
960 | }
961 | nodes {
962 | position {
963 | x: 0.0
964 | y: 0.0
965 | z: 0.0
966 | w: 1.0
967 | }
968 | rotation {
969 | x: 0.0
970 | y: 0.0
971 | z: 0.0
972 | w: 1.0
973 | }
974 | scale {
975 | x: 1.0
976 | y: 1.0
977 | z: 1.0
978 | w: 1.0
979 | }
980 | size {
981 | x: 300.0
982 | y: 88.0
983 | z: 0.0
984 | w: 1.0
985 | }
986 | color {
987 | x: 1.0
988 | y: 1.0
989 | z: 1.0
990 | w: 1.0
991 | }
992 | type: TYPE_BOX
993 | blend_mode: BLEND_MODE_ALPHA
994 | texture: "button/button_normal"
995 | id: "subscription/larrybutton"
996 | xanchor: XANCHOR_NONE
997 | yanchor: YANCHOR_NONE
998 | pivot: PIVOT_CENTER
999 | adjust_mode: ADJUST_MODE_FIT
1000 | parent: "subscription"
1001 | layer: ""
1002 | inherit_alpha: true
1003 | slice9 {
1004 | x: 32.0
1005 | y: 32.0
1006 | z: 32.0
1007 | w: 32.0
1008 | }
1009 | clipping_mode: CLIPPING_MODE_NONE
1010 | clipping_visible: true
1011 | clipping_inverted: false
1012 | alpha: 1.0
1013 | template_node_child: true
1014 | size_mode: SIZE_MODE_MANUAL
1015 | custom_type: 0
1016 | enabled: true
1017 | visible: true
1018 | material: ""
1019 | }
1020 | nodes {
1021 | position {
1022 | x: 0.0
1023 | y: 0.0
1024 | z: 0.0
1025 | w: 1.0
1026 | }
1027 | rotation {
1028 | x: 0.0
1029 | y: 0.0
1030 | z: 0.0
1031 | w: 1.0
1032 | }
1033 | scale {
1034 | x: 1.0
1035 | y: 1.0
1036 | z: 1.0
1037 | w: 1.0
1038 | }
1039 | size {
1040 | x: 200.0
1041 | y: 100.0
1042 | z: 0.0
1043 | w: 1.0
1044 | }
1045 | color {
1046 | x: 1.0
1047 | y: 1.0
1048 | z: 1.0
1049 | w: 1.0
1050 | }
1051 | type: TYPE_TEXT
1052 | blend_mode: BLEND_MODE_ALPHA
1053 | text: "Subscription"
1054 | font: "larryfont"
1055 | id: "subscription/larrylabel"
1056 | xanchor: XANCHOR_NONE
1057 | yanchor: YANCHOR_NONE
1058 | pivot: PIVOT_CENTER
1059 | outline {
1060 | x: 0.0
1061 | y: 0.0
1062 | z: 0.0
1063 | w: 1.0
1064 | }
1065 | shadow {
1066 | x: 1.0
1067 | y: 1.0
1068 | z: 1.0
1069 | w: 1.0
1070 | }
1071 | adjust_mode: ADJUST_MODE_FIT
1072 | line_break: false
1073 | parent: "subscription/larrybutton"
1074 | layer: ""
1075 | inherit_alpha: true
1076 | alpha: 1.0
1077 | outline_alpha: 1.0
1078 | shadow_alpha: 1.0
1079 | overridden_fields: 8
1080 | template_node_child: true
1081 | text_leading: 1.0
1082 | text_tracking: 0.0
1083 | custom_type: 0
1084 | enabled: true
1085 | visible: true
1086 | material: ""
1087 | }
1088 | nodes {
1089 | position {
1090 | x: 320.0
1091 | y: 56.0
1092 | z: 0.0
1093 | w: 1.0
1094 | }
1095 | rotation {
1096 | x: 0.0
1097 | y: 0.0
1098 | z: 0.0
1099 | w: 1.0
1100 | }
1101 | scale {
1102 | x: 1.0
1103 | y: 1.0
1104 | z: 1.0
1105 | w: 1.0
1106 | }
1107 | size {
1108 | x: 200.0
1109 | y: 100.0
1110 | z: 0.0
1111 | w: 1.0
1112 | }
1113 | color {
1114 | x: 1.0
1115 | y: 1.0
1116 | z: 1.0
1117 | w: 1.0
1118 | }
1119 | type: TYPE_TEMPLATE
1120 | id: "pending"
1121 | layer: ""
1122 | inherit_alpha: true
1123 | alpha: 1.0
1124 | template: "/dirtylarry/button.gui"
1125 | template_node_child: false
1126 | custom_type: 0
1127 | enabled: true
1128 | }
1129 | nodes {
1130 | position {
1131 | x: 0.0
1132 | y: 0.0
1133 | z: 0.0
1134 | w: 1.0
1135 | }
1136 | rotation {
1137 | x: 0.0
1138 | y: 0.0
1139 | z: 0.0
1140 | w: 1.0
1141 | }
1142 | scale {
1143 | x: 1.0
1144 | y: 1.0
1145 | z: 1.0
1146 | w: 1.0
1147 | }
1148 | size {
1149 | x: 200.0
1150 | y: 88.0
1151 | z: 0.0
1152 | w: 1.0
1153 | }
1154 | color {
1155 | x: 1.0
1156 | y: 1.0
1157 | z: 1.0
1158 | w: 1.0
1159 | }
1160 | type: TYPE_BOX
1161 | blend_mode: BLEND_MODE_ALPHA
1162 | texture: "button/button_normal"
1163 | id: "pending/larrybutton"
1164 | xanchor: XANCHOR_NONE
1165 | yanchor: YANCHOR_NONE
1166 | pivot: PIVOT_CENTER
1167 | adjust_mode: ADJUST_MODE_FIT
1168 | parent: "pending"
1169 | layer: ""
1170 | inherit_alpha: true
1171 | slice9 {
1172 | x: 32.0
1173 | y: 32.0
1174 | z: 32.0
1175 | w: 32.0
1176 | }
1177 | clipping_mode: CLIPPING_MODE_NONE
1178 | clipping_visible: true
1179 | clipping_inverted: false
1180 | alpha: 1.0
1181 | overridden_fields: 4
1182 | template_node_child: true
1183 | size_mode: SIZE_MODE_MANUAL
1184 | custom_type: 0
1185 | enabled: true
1186 | visible: true
1187 | material: ""
1188 | }
1189 | nodes {
1190 | position {
1191 | x: 0.0
1192 | y: 0.0
1193 | z: 0.0
1194 | w: 1.0
1195 | }
1196 | rotation {
1197 | x: 0.0
1198 | y: 0.0
1199 | z: 0.0
1200 | w: 1.0
1201 | }
1202 | scale {
1203 | x: 1.0
1204 | y: 1.0
1205 | z: 1.0
1206 | w: 1.0
1207 | }
1208 | size {
1209 | x: 200.0
1210 | y: 100.0
1211 | z: 0.0
1212 | w: 1.0
1213 | }
1214 | color {
1215 | x: 1.0
1216 | y: 1.0
1217 | z: 1.0
1218 | w: 1.0
1219 | }
1220 | type: TYPE_TEXT
1221 | blend_mode: BLEND_MODE_ALPHA
1222 | text: "Pending"
1223 | font: "larryfont"
1224 | id: "pending/larrylabel"
1225 | xanchor: XANCHOR_NONE
1226 | yanchor: YANCHOR_NONE
1227 | pivot: PIVOT_CENTER
1228 | outline {
1229 | x: 0.0
1230 | y: 0.0
1231 | z: 0.0
1232 | w: 1.0
1233 | }
1234 | shadow {
1235 | x: 1.0
1236 | y: 1.0
1237 | z: 1.0
1238 | w: 1.0
1239 | }
1240 | adjust_mode: ADJUST_MODE_FIT
1241 | line_break: false
1242 | parent: "pending/larrybutton"
1243 | layer: ""
1244 | inherit_alpha: true
1245 | alpha: 1.0
1246 | outline_alpha: 1.0
1247 | shadow_alpha: 1.0
1248 | overridden_fields: 8
1249 | template_node_child: true
1250 | text_leading: 1.0
1251 | text_tracking: 0.0
1252 | custom_type: 0
1253 | enabled: true
1254 | visible: true
1255 | material: ""
1256 | }
1257 | nodes {
1258 | position {
1259 | x: 40.0
1260 | y: 153.0
1261 | z: 0.0
1262 | w: 1.0
1263 | }
1264 | rotation {
1265 | x: 0.0
1266 | y: 0.0
1267 | z: 0.0
1268 | w: 1.0
1269 | }
1270 | scale {
1271 | x: 1.0
1272 | y: 1.0
1273 | z: 1.0
1274 | w: 1.0
1275 | }
1276 | size {
1277 | x: 200.0
1278 | y: 100.0
1279 | z: 0.0
1280 | w: 1.0
1281 | }
1282 | color {
1283 | x: 1.0
1284 | y: 1.0
1285 | z: 1.0
1286 | w: 1.0
1287 | }
1288 | type: TYPE_TEMPLATE
1289 | id: "chk_finish"
1290 | layer: ""
1291 | inherit_alpha: true
1292 | alpha: 1.0
1293 | template: "/dirtylarry/checkbox_label.gui"
1294 | template_node_child: false
1295 | custom_type: 0
1296 | enabled: true
1297 | }
1298 | nodes {
1299 | position {
1300 | x: 0.0
1301 | y: 0.0
1302 | z: 0.0
1303 | w: 1.0
1304 | }
1305 | rotation {
1306 | x: 0.0
1307 | y: 0.0
1308 | z: 0.0
1309 | w: 1.0
1310 | }
1311 | scale {
1312 | x: 1.0
1313 | y: 1.0
1314 | z: 1.0
1315 | w: 1.0
1316 | }
1317 | size {
1318 | x: 64.0
1319 | y: 68.0
1320 | z: 0.0
1321 | w: 1.0
1322 | }
1323 | color {
1324 | x: 1.0
1325 | y: 1.0
1326 | z: 1.0
1327 | w: 1.0
1328 | }
1329 | type: TYPE_BOX
1330 | blend_mode: BLEND_MODE_ALPHA
1331 | texture: "checkbox/checkbox_normal"
1332 | id: "chk_finish/larrycheckbox"
1333 | xanchor: XANCHOR_NONE
1334 | yanchor: YANCHOR_NONE
1335 | pivot: PIVOT_CENTER
1336 | adjust_mode: ADJUST_MODE_FIT
1337 | parent: "chk_finish"
1338 | layer: ""
1339 | inherit_alpha: true
1340 | slice9 {
1341 | x: 0.0
1342 | y: 0.0
1343 | z: 0.0
1344 | w: 0.0
1345 | }
1346 | clipping_mode: CLIPPING_MODE_NONE
1347 | clipping_visible: true
1348 | clipping_inverted: false
1349 | alpha: 1.0
1350 | template_node_child: true
1351 | size_mode: SIZE_MODE_MANUAL
1352 | custom_type: 0
1353 | enabled: true
1354 | visible: true
1355 | material: ""
1356 | }
1357 | nodes {
1358 | position {
1359 | x: 46.0
1360 | y: 3.0
1361 | z: 0.0
1362 | w: 1.0
1363 | }
1364 | rotation {
1365 | x: 0.0
1366 | y: 0.0
1367 | z: 0.0
1368 | w: 1.0
1369 | }
1370 | scale {
1371 | x: 1.0
1372 | y: 1.0
1373 | z: 1.0
1374 | w: 1.0
1375 | }
1376 | size {
1377 | x: 200.0
1378 | y: 50.0
1379 | z: 0.0
1380 | w: 1.0
1381 | }
1382 | color {
1383 | x: 1.0
1384 | y: 1.0
1385 | z: 1.0
1386 | w: 1.0
1387 | }
1388 | type: TYPE_TEXT
1389 | blend_mode: BLEND_MODE_ALPHA
1390 | text: "Finish"
1391 | font: "larryfont"
1392 | id: "chk_finish/larrylabel"
1393 | xanchor: XANCHOR_NONE
1394 | yanchor: YANCHOR_NONE
1395 | pivot: PIVOT_W
1396 | outline {
1397 | x: 1.0
1398 | y: 1.0
1399 | z: 1.0
1400 | w: 1.0
1401 | }
1402 | shadow {
1403 | x: 1.0
1404 | y: 1.0
1405 | z: 1.0
1406 | w: 1.0
1407 | }
1408 | adjust_mode: ADJUST_MODE_FIT
1409 | line_break: false
1410 | parent: "chk_finish"
1411 | layer: ""
1412 | inherit_alpha: true
1413 | alpha: 1.0
1414 | outline_alpha: 1.0
1415 | shadow_alpha: 1.0
1416 | overridden_fields: 8
1417 | template_node_child: true
1418 | text_leading: 1.0
1419 | text_tracking: 0.0
1420 | custom_type: 0
1421 | enabled: true
1422 | visible: true
1423 | material: ""
1424 | }
1425 | nodes {
1426 | position {
1427 | x: 362.0
1428 | y: 153.0
1429 | z: 0.0
1430 | w: 1.0
1431 | }
1432 | rotation {
1433 | x: 0.0
1434 | y: 0.0
1435 | z: 0.0
1436 | w: 1.0
1437 | }
1438 | scale {
1439 | x: 1.0
1440 | y: 1.0
1441 | z: 1.0
1442 | w: 1.0
1443 | }
1444 | size {
1445 | x: 200.0
1446 | y: 100.0
1447 | z: 0.0
1448 | w: 1.0
1449 | }
1450 | color {
1451 | x: 1.0
1452 | y: 1.0
1453 | z: 1.0
1454 | w: 1.0
1455 | }
1456 | type: TYPE_TEMPLATE
1457 | id: "chk_acknowledge"
1458 | layer: ""
1459 | inherit_alpha: true
1460 | alpha: 1.0
1461 | template: "/dirtylarry/checkbox_label.gui"
1462 | template_node_child: false
1463 | custom_type: 0
1464 | enabled: true
1465 | }
1466 | nodes {
1467 | position {
1468 | x: 0.0
1469 | y: 0.0
1470 | z: 0.0
1471 | w: 1.0
1472 | }
1473 | rotation {
1474 | x: 0.0
1475 | y: 0.0
1476 | z: 0.0
1477 | w: 1.0
1478 | }
1479 | scale {
1480 | x: 1.0
1481 | y: 1.0
1482 | z: 1.0
1483 | w: 1.0
1484 | }
1485 | size {
1486 | x: 64.0
1487 | y: 68.0
1488 | z: 0.0
1489 | w: 1.0
1490 | }
1491 | color {
1492 | x: 1.0
1493 | y: 1.0
1494 | z: 1.0
1495 | w: 1.0
1496 | }
1497 | type: TYPE_BOX
1498 | blend_mode: BLEND_MODE_ALPHA
1499 | texture: "checkbox/checkbox_normal"
1500 | id: "chk_acknowledge/larrycheckbox"
1501 | xanchor: XANCHOR_NONE
1502 | yanchor: YANCHOR_NONE
1503 | pivot: PIVOT_CENTER
1504 | adjust_mode: ADJUST_MODE_FIT
1505 | parent: "chk_acknowledge"
1506 | layer: ""
1507 | inherit_alpha: true
1508 | slice9 {
1509 | x: 0.0
1510 | y: 0.0
1511 | z: 0.0
1512 | w: 0.0
1513 | }
1514 | clipping_mode: CLIPPING_MODE_NONE
1515 | clipping_visible: true
1516 | clipping_inverted: false
1517 | alpha: 1.0
1518 | template_node_child: true
1519 | size_mode: SIZE_MODE_MANUAL
1520 | custom_type: 0
1521 | enabled: true
1522 | visible: true
1523 | material: ""
1524 | }
1525 | nodes {
1526 | position {
1527 | x: 46.0
1528 | y: 3.0
1529 | z: 0.0
1530 | w: 1.0
1531 | }
1532 | rotation {
1533 | x: 0.0
1534 | y: 0.0
1535 | z: 0.0
1536 | w: 1.0
1537 | }
1538 | scale {
1539 | x: 1.0
1540 | y: 1.0
1541 | z: 1.0
1542 | w: 1.0
1543 | }
1544 | size {
1545 | x: 200.0
1546 | y: 50.0
1547 | z: 0.0
1548 | w: 1.0
1549 | }
1550 | color {
1551 | x: 1.0
1552 | y: 1.0
1553 | z: 1.0
1554 | w: 1.0
1555 | }
1556 | type: TYPE_TEXT
1557 | blend_mode: BLEND_MODE_ALPHA
1558 | text: "Acknowledge"
1559 | font: "larryfont"
1560 | id: "chk_acknowledge/larrylabel"
1561 | xanchor: XANCHOR_NONE
1562 | yanchor: YANCHOR_NONE
1563 | pivot: PIVOT_W
1564 | outline {
1565 | x: 1.0
1566 | y: 1.0
1567 | z: 1.0
1568 | w: 1.0
1569 | }
1570 | shadow {
1571 | x: 1.0
1572 | y: 1.0
1573 | z: 1.0
1574 | w: 1.0
1575 | }
1576 | adjust_mode: ADJUST_MODE_FIT
1577 | line_break: false
1578 | parent: "chk_acknowledge"
1579 | layer: ""
1580 | inherit_alpha: true
1581 | alpha: 1.0
1582 | outline_alpha: 1.0
1583 | shadow_alpha: 1.0
1584 | overridden_fields: 8
1585 | template_node_child: true
1586 | text_leading: 1.0
1587 | text_tracking: 0.0
1588 | custom_type: 0
1589 | enabled: true
1590 | visible: true
1591 | material: ""
1592 | }
1593 | material: "/builtins/materials/gui.material"
1594 | adjust_reference: ADJUST_REFERENCE_PARENT
1595 | max_nodes: 512
1596 |
--------------------------------------------------------------------------------
/main/main.gui_script:
--------------------------------------------------------------------------------
1 | local dirtylarry = require "dirtylarry/dirtylarry"
2 |
3 | local GOLDBARS_SMALL = "com.defold.iap.goldbar.small"
4 | local GOLDBARS_MEDIUM = "com.defold.iap.goldbar.medium"
5 | local GOLDBARS_LARGE = "com.defold.iap.goldbar.large"
6 | local SUBSCRIPTION = "com.defold.iap.subscription.one"
7 | local NON_CONSUMABLE = "com.defold.iap.removeads"
8 |
9 | local items = {
10 | GOLDBARS_SMALL,
11 | GOLDBARS_MEDIUM,
12 | GOLDBARS_LARGE,
13 | SUBSCRIPTION,
14 | }
15 |
16 | -- mapping between product id and button name
17 | local item_buttons = {
18 | [GOLDBARS_SMALL] = "goldbars_small",
19 | [GOLDBARS_MEDIUM] = "goldbars_medium",
20 | [GOLDBARS_LARGE] = "goldbars_large",
21 | [SUBSCRIPTION] = "subscription",
22 | }
23 |
24 | local available_items = {}
25 |
26 | local LOG = {}
27 |
28 | local function log(fmt, ...)
29 | if not fmt then return end
30 | local line = fmt:format(...)
31 | print(line)
32 | table.insert(LOG, line)
33 | if #LOG > 10 then
34 | table.remove(LOG, 1)
35 | end
36 | local s = table.concat(LOG, "\n")
37 | gui.set_text(gui.get_node("log"), s)
38 | end
39 |
40 | local function process_pending_transactions()
41 | log("iap.process_pending_transactions()")
42 | iap.process_pending_transactions()
43 | end
44 |
45 | local function buy(id)
46 | log("iap.buy() " .. id)
47 | local options = {}
48 | local item = available_items[id]
49 | if item.subscriptions then
50 | local subscription = item.subscriptions[1]
51 | options.token = subscription.token
52 | end
53 | iap.buy(id, options)
54 | end
55 |
56 | local function restore()
57 | log("iap.restore()")
58 | iap.restore()
59 | end
60 |
61 | local function list()
62 | log("iap.list()")
63 | for item, button in pairs(item_buttons) do
64 | gui.set_color(gui.get_node(button.."/larrylabel"), vmath.vector4(1,1,1,0.5))
65 | end
66 | iap.list(items, function(self, products, error)
67 | if error then
68 | log(error.error)
69 | return
70 | end
71 |
72 | for k,p in pairs(products) do
73 | available_items[p.ident] = p
74 | log("Item %s", p.ident)
75 | pprint(p)
76 | local button = item_buttons[p.ident]
77 | if button then
78 | gui.set_color(gui.get_node(button.."/larrylabel"), vmath.vector4(1,1,1,1))
79 | else
80 | log("Unable to find button for %s", tostring(p.ident))
81 | end
82 | end
83 | end)
84 | end
85 |
86 |
87 | local function buy_listener(self, transaction, error)
88 | pprint(transaction)
89 | if error then
90 | log("iap.buy() error %s - %s", tostring(error.error), tostring(error.reason))
91 | return
92 | end
93 |
94 | if iap.get_provider_id() == iap.PROVIDER_ID_GOOGLE and transaction.ident == NON_CONSUMABLE then
95 | log("iap.buy() ok - google")
96 | gui.set_color(gui.get_node("reset/larrylabel"), vmath.vector4(1,1,1,1))
97 | product_items["reset"] = transaction
98 | else
99 | log("iap.buy() ok %s", transaction.ident)
100 | if self.finish then
101 | log("iap.finish() %s", transaction.ident)
102 | iap.finish(transaction)
103 | elseif self.acknowledge then
104 | log("iap.acknowledge() %s", transaction.ident)
105 | iap.acknowledge(transaction)
106 | end
107 | end
108 | end
109 |
110 | function init(self)
111 | self.log = {}
112 | self.finish = false
113 | self.acknowledge = false
114 | log("init()")
115 | msg.post(".", "acquire_input_focus")
116 | if not iap then
117 | log("In-App Purchases not supported")
118 | return
119 | end
120 |
121 | list()
122 | iap.set_listener(buy_listener)
123 | end
124 |
125 | function on_input(self, action_id, action)
126 | if action_id then
127 | for item, button in pairs(item_buttons) do
128 | if available_items[item] then
129 | dirtylarry:button(button, action_id, action, function()
130 | buy(item)
131 | end)
132 | end
133 | end
134 | dirtylarry:button("list", action_id, action, function()
135 | list()
136 | end)
137 | dirtylarry:button("restore", action_id, action, function()
138 | restore()
139 | end)
140 | dirtylarry:button("pending", action_id, action, function()
141 | process_pending_transactions()
142 | end)
143 | self.finish = dirtylarry:checkbox("chk_finish", action_id, action, self.finish)
144 | self.acknowledge = dirtylarry:checkbox("chk_acknowledge", action_id, action, self.acknowledge)
145 | end
146 | end
147 |
--------------------------------------------------------------------------------