├── .gitignore
├── LICENSE
├── README-en.md
├── README.md
└── unity-iap
├── objc
├── External
│ ├── DYFIndefiniteAnimatedSpinner.h
│ ├── DYFIndefiniteAnimatedSpinner.m
│ ├── DYFLoadingView.h
│ ├── DYFLoadingView.m
│ ├── NSObject+DYFAdd.h
│ ├── NSObject+DYFAdd.m
│ ├── UIView+DYFAdd.h
│ └── UIView+DYFAdd.m
├── StoreManager
│ ├── DYFStoreManager.h
│ └── DYFStoreManager.mm
├── UnityIAPConnector.h
└── UnityIAPConnector.mm
└── unity
└── UnityIAPManager.cs
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # .NET Core
46 | project.lock.json
47 | project.fragment.lock.json
48 | artifacts/
49 | **/Properties/launchSettings.json
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # Visual Studio code coverage results
117 | *.coverage
118 | *.coveragexml
119 |
120 | # NCrunch
121 | _NCrunch_*
122 | .*crunch*.local.xml
123 | nCrunchTemp_*
124 |
125 | # MightyMoose
126 | *.mm.*
127 | AutoTest.Net/
128 |
129 | # Web workbench (sass)
130 | .sass-cache/
131 |
132 | # Installshield output folder
133 | [Ee]xpress/
134 |
135 | # DocProject is a documentation generator add-in
136 | DocProject/buildhelp/
137 | DocProject/Help/*.HxT
138 | DocProject/Help/*.HxC
139 | DocProject/Help/*.hhc
140 | DocProject/Help/*.hhk
141 | DocProject/Help/*.hhp
142 | DocProject/Help/Html2
143 | DocProject/Help/html
144 |
145 | # Click-Once directory
146 | publish/
147 |
148 | # Publish Web Output
149 | *.[Pp]ublish.xml
150 | *.azurePubxml
151 | # TODO: Comment the next line if you want to checkin your web deploy settings
152 | # but database connection strings (with potential passwords) will be unencrypted
153 | *.pubxml
154 | *.publishproj
155 |
156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
157 | # checkin your Azure Web App publish settings, but sensitive information contained
158 | # in these scripts will be unencrypted
159 | PublishScripts/
160 |
161 | # NuGet Packages
162 | *.nupkg
163 | # The packages folder can be ignored because of Package Restore
164 | **/packages/*
165 | # except build/, which is used as an MSBuild target.
166 | !**/packages/build/
167 | # Uncomment if necessary however generally it will be regenerated when needed
168 | #!**/packages/repositories.config
169 | # NuGet v3's project.json files produces more ignorable files
170 | *.nuget.props
171 | *.nuget.targets
172 |
173 | # Microsoft Azure Build Output
174 | csx/
175 | *.build.csdef
176 |
177 | # Microsoft Azure Emulator
178 | ecf/
179 | rcf/
180 |
181 | # Windows Store app package directories and files
182 | AppPackages/
183 | BundleArtifacts/
184 | Package.StoreAssociation.xml
185 | _pkginfo.txt
186 |
187 | # Visual Studio cache files
188 | # files ending in .cache can be ignored
189 | *.[Cc]ache
190 | # but keep track of directories ending in .cache
191 | !*.[Cc]ache/
192 |
193 | # Others
194 | ClientBin/
195 | ~$*
196 | *~
197 | *.dbmdl
198 | *.dbproj.schemaview
199 | *.jfm
200 | *.pfx
201 | *.publishsettings
202 | orleans.codegen.cs
203 |
204 | # Since there are multiple workflows, uncomment next line to ignore bower_components
205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
206 | #bower_components/
207 |
208 | # RIA/Silverlight projects
209 | Generated_Code/
210 |
211 | #Visual Studio LightSwitch
212 | _Pvt_Extensions/
213 | GeneratedArtifacts/
214 | ServiceConfiguration.cscfg
215 | ModelManifest.xml
216 | generated.parameters.xml
217 | ## TODO: Comment the next line if you want version controls the generated client .xap file
218 | *.Client.xap
219 |
220 | # Backup & report files from converting an old project file
221 | # to a newer Visual Studio version. Backup files are not needed,
222 | # because we have git ;-)
223 | _UpgradeReport_Files/
224 | Backup*/
225 | UpgradeLog*.XML
226 | UpgradeLog*.htm
227 |
228 | # SQL Server files
229 | *.mdf
230 | *.ldf
231 |
232 | # Business Intelligence projects
233 | *.rdl.data
234 | *.bim.layout
235 | *.bim_*.settings
236 |
237 | # Microsoft Fakes
238 | FakesAssemblies/
239 |
240 | # GhostDoc plugin setting file
241 | *.GhostDoc.xml
242 |
243 | # Node.js Tools for Visual Studio
244 | .ntvs_analysis.dat
245 | node_modules/
246 |
247 | # Typescript v1 declaration files
248 | typings/
249 |
250 | # Visual Studio 6 build log
251 | *.plg
252 |
253 | # Visual Studio 6 workspace options file
254 | *.opt
255 |
256 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
257 | *.vbw
258 |
259 | # Visual Studio LightSwitch build output
260 | **/*.HTMLClient/GeneratedArtifacts
261 | **/*.DesktopClient/GeneratedArtifacts
262 | **/*.DesktopClient/ModelManifest.xml
263 | **/*.Server/GeneratedArtifacts
264 | **/*.Server/ModelManifest.xml
265 | _Pvt_Extensions
266 |
267 | # Paket dependency manager
268 | .paket/paket.exe
269 | paket-files/
270 |
271 | # FAKE - F# Make
272 | .fake/
273 |
274 | # JetBrains Rider
275 | .idea/
276 | *.sln.iml
277 |
278 | # CodeRush
279 | .cr/
280 |
281 | # Python Tools for Visual Studio (PTVS)
282 | __pycache__/
283 | *.pyc
284 |
285 | # Mac crap
286 | .DS_Store
287 |
288 | # Cocoapods
289 | Pods/
290 | Podfile.lock
291 |
292 | # VisualStudioCode
293 | .vscode
294 |
295 | # Cake - Uncomment if you are using it
296 | # tools/**
297 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Created by Tenfay on 2020/4/16. ( https://github.com/itenfay/Unity-iOS-InAppPurchase )
4 | Copyright © 2020 Tenfay. All rights reserved.
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README-en.md:
--------------------------------------------------------------------------------
1 | [中文版](README.md) | **English Version**
2 |
3 |
4 | ## Unity-iOS-InAppPurchase
5 |
6 | Unity implements Apple's in-app purchases for iOS.
7 |
8 | [](LICENSE)
9 |
10 |
11 | ## Group (ID:614799921)
12 |
13 |
14 |

15 |
16 |
17 |
18 | ## Usage
19 |
20 | The directory structure of `unity-iap` is as follows:
21 |
22 | - **Objective-C**
23 |
24 | | Dir | file |
25 | | :------------------------: | :----------------: |
26 | | objc(StoreManager External)| UnityIAPConnector.h/.mm |
27 | | StoreManager | DYFStoreManager.h/.mm |
28 | | External(Optional) | DYFLoadingView.h/.m DYFIndefiniteAnimatedSpinner.h/.m NSObject+DYFAdd.h/.m UIView+DYFAdd.h/.m |
29 |
30 | - **Unity**
31 |
32 | | Dir | file |
33 | | :----------------: | :----------------: |
34 | | unity | UnityIAPManager.cs |
35 |
36 | > **Note: Unity needs to show/hide loading panel or show prompt message at the right time.**
37 |
38 | ### 1. Add the required files for Objective-C.
39 |
40 | You need to add the required files for Objective-C in Unity project
41 |
42 | ### 2. Add cs script.
43 |
44 | You need to add the required cs script of in-app purchase for iOS in Unity project.
45 |
46 | ### 3、Add `DYFStoreKit` directory files.
47 |
48 | Use `pod 'DYFStoreKit'` to add the latest version of in-app purchas library for iOS.
49 |
50 | Or
51 |
52 | Clone `DYFStoreKit` (git clone https://github.com/itenfay/DYFStoreKit.git) to the local directory.
53 |
54 | ### 4、Adds the transaction observer and others.
55 |
56 | Adds header file `#import "DYFStoreManager.h"` in UnityAppController.mm.
57 |
58 | - Comply with the agreement.
59 |
60 | ```
61 | @interface UnityAppController()
62 |
63 | @end
64 | ```
65 |
66 | - Adds the observer, set up the delegate and data persistence.
67 |
68 | As long as you add the following a piece of code before the method return value, the rest of the code does not change.
69 |
70 | ```
71 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
72 | {
73 | [self initIAPSDK];
74 | return YES;
75 | }
76 |
77 | - (void)initIAPSDK
78 | {
79 | [DYFStoreManager.shared addStoreObserver];
80 |
81 | // Adds an observer that responds to updated transactions to the payment queue.
82 | // If an application quits when transactions are still being processed, those transactions are not lost. The next time the application launches, the payment queue will resume processing the transactions. Your application should always expect to be notified of completed transactions.
83 | // If more than one transaction observer is attached to the payment queue, no guarantees are made as to the order they will be called in. It is recommended that you use a single observer to process and finish the transaction.
84 | [DYFStore.defaultStore addPaymentTransactionObserver];
85 |
86 | // Sets the delegate processes the purchase which was initiated by user from the App Store.
87 | DYFStore.defaultStore.delegate = self;
88 | }
89 | ```
90 |
91 | - You can process the purchase which was initiated by user from the App Store. (iOS 11.0+)
92 |
93 | ```
94 | // Processes the purchase which was initiated by user from the App Store.
95 | - (void)didReceiveAppStorePurchaseRequest:(SKPaymentQueue *)queue payment:(SKPayment *)payment forProduct:(SKProduct *)product
96 | {
97 | if (![DYFStore canMakePayments]) {
98 | // Tips: Your device is not able or allowed to make payments!
99 | return;
100 | }
101 |
102 | // Get account name from your own user system.
103 | NSString *accountName = @"Handsome Jon";
104 | // This algorithm is negotiated with server developer.
105 | NSString *userIdentifier = DYFStore_supplySHA256(accountName);
106 | DYFStoreLog(@"userIdentifier: %@", userIdentifier);
107 |
108 | [DYFStoreManager.shared addPayment:product.productIdentifier userIdentifier:userIdentifier];
109 | }
110 | ```
111 |
112 | ### 5. Points for attention.
113 |
114 | - Initializes the unity callback game object and function.
115 |
116 | ```
117 | public void initUnityMsgCallback(string gameObject, string func)
118 | {
119 | LogManager.Log("initUnityMsgCallback=" + gameObject + "," + func);
120 | #if !UNITY_EDITOR && UNITY_IOS
121 | DYFInitUnityMsgCallback(gameObject, func);
122 | #endif
123 | }
124 | ```
125 |
126 | - The purchase of a single product.
127 |
128 | If your app contains purchased UI interface and product display information, users only need to choose to purchase. The disadvantage is that the program has to do local price matching.
129 |
130 | ```
131 | public void retrieveProduct(string productId)
132 | {
133 | if (Application.platform != RuntimePlatform.OSXEditor) {
134 | LogManager.Log("retrieveProduct=" + productId);
135 | #if !UNITY_EDITOR && UNITY_IOS
136 | // Tips: show loading panel.
137 | DYFRetrieveProductFromAppStore(productId);
138 | #endif
139 | }
140 | }
141 | ```
142 |
143 | When the program gets a successful callback, you can parse the data and add the payment. In other cases, the program should prompt the user with a pop-up box.
144 |
145 | ```
146 | case (int)CallbackType.Action_GetProductSuccessfully:
147 | {
148 | LogManager.Log ("CallbackType.Action_GetProductSuccessfully");
149 | string productId = (string)json["msg_data"]["p_id"];
150 | // You get this from your user system when you need it.
151 | string userId = null;
152 | addPayment(productId, userId);
153 | break;
154 | }
155 | ````
156 |
157 | - Requests multiple products and display the purchased UI interface.
158 |
159 | You can request multiple products at a time and get the localized information of the product through the tool.
160 |
161 | ```
162 | // You can either return the value or get a set of product identifiers from your server.
163 | private JArray getJArrayOfProductIds()
164 | {
165 | JArray a = new JArray();
166 | foreach (var obj in LaunchMng.IAPDict) {
167 | a.Add(obj.Value);
168 | }
169 | return a;
170 | }
171 |
172 | // Converts JArray to json string.
173 | private string getJsonOfProductIds()
174 | {
175 | JArray a = getJArrayOfProductIds();
176 | string json = JsonConvert.SerializeObject(a);
177 | return json;
178 | }
179 |
180 | public void retrieveProducts()
181 | {
182 | if (Application.platform != RuntimePlatform.OSXEditor) {
183 | string jsonOfProductIds = getJsonOfProductIds()
184 | LogManager.Log("retrieveProducts=" + jsonOfProductIds);
185 | #if !UNITY_EDITOR && UNITY_IOS
186 | // Tips: show loading panel.
187 | DYFRetrieveProductsFromAppStore(jsonOfProductIds);
188 | #endif
189 | }
190 | }
191 | ```
192 |
193 | When the program gets a successful callback, it can parse the data to display the UI interface of the purchase. In other cases, the program should prompt the user with a pop-up box.
194 |
195 | ```
196 | case (int)CallbackType.Action_GetProductsSuccessfully:
197 | {
198 | LogManager.Log ("CallbackType.Action_GetProductsSuccessfully");
199 | JArray arr = JArray.Parse(json["msg_data"].ToString());
200 | parseProductList(arr);
201 | break;
202 | }
203 |
204 | private void parseProductList(JArray jarr)
205 | {
206 | try {
207 | LogManager.Log ("parseProductList... jarr: " + jarr.ToString());
208 | for(int i = 0; i < jarr.Count; i++) {
209 | JObject jo = JObject.Parse(jarr[i].ToString());
210 | string productId = jo["p_id"].ToString();
211 | string title = jo["p_title"].ToString();
212 | string price = jo["p_price"].ToString();
213 | string localizedPrice = jo["p_localized_price"].ToString();
214 | string localizedDesc = jo["p_localized_desc"].ToString();
215 |
216 | LogManager.Log ("ProductList..." + i.ToString() + " productId: " + productId +
217 | "; title: " + title + "; price: " + price + "; localizedPrice: " +
218 | localizedPrice + "; localizedDesc: " + localizedDesc);
219 | }
220 | // Call displayStorePanel(), The parameters need to be defined by you.
221 | // Waiting for the user to choose to buy goods, then call addPayment(...)
222 | } catch (System.Exception e) {
223 | LogManager.Log (e.ToString (), LogType.Fatal);
224 | }
225 | }
226 |
227 | private void displayStorePanel()
228 | {
229 | // After getting the products, then the store panel is displayed.
230 | }
231 | ```
232 |
233 | The user chooses to purchase product, and the user ID can be set as needed.
234 |
235 | ```
236 | public void addPayment(string productId, string userId)
237 | {
238 | if (Application.platform != RuntimePlatform.OSXEditor) {
239 | LogManager.Log("addPayment=" + productId + "," + userId);
240 | #if !UNITY_EDITOR && UNITY_IOS
241 | // Tips: show loading panel.
242 | DYFAddPayment(productId, userId);
243 | #endif
244 | }
245 | }
246 | ```
247 |
248 | - Restores the completed transactions, user ID is optional.
249 |
250 | ```
251 | public void restoreTransactions(string userId)
252 | {
253 | if (Application.platform != RuntimePlatform.OSXEditor) {
254 | LogManager.Log("DYFRestoreTransactions=", userId);
255 | #if !UNITY_EDITOR && UNITY_IOS
256 | // Tips: show loading panel.
257 | DYFRestoreTransactions(userId);
258 | #endif
259 | LogManager.Log("Store start restoring completed transactions...", LogType.Normal);
260 | }
261 | }
262 | ```
263 |
264 | - Refreshes receipt.
265 |
266 | If the receipt is invalid or missing, refresh the App Store's receipt.
267 |
268 | ```
269 | case(int)CallbackType.Action_RefreshReceipt:
270 | {
271 | LogManager.Log ("CallbackType.Action_RefreshReceipt");
272 | // Tips: The receipt needs to be refreshed.
273 | string desc = (string)json["msg_data"]["m_desc"];
274 | LogManager.Log("err_desc=", desc);
275 | refreshReceipt()
276 | break;
277 | }
278 | ```
279 |
280 | ```
281 | public void refreshReceipt()
282 | {
283 | if (Application.platform != RuntimePlatform.OSXEditor) {
284 | #if !UNITY_EDITOR && UNITY_IOS
285 | // Tips: show loading panel.
286 | DYFRefreshReceipt();
287 | #endif
288 | LogManager.Log("Store start refreshing receipt...", LogType.Normal);
289 | }
290 | }
291 | ```
292 |
293 | - Receipt verification.
294 |
295 | ```
296 | private void verifyReceipt(JObject jo)
297 | {
298 | try {
299 | LogManager.Log ("verifyReceipt... jo: " + jo.ToString());
300 | int state = int.Parse(jo["t_state"].ToString);
301 | string productId = jo["p_id"].ToString();
302 | string userId = jo["u_id"].ToString();
303 | string transId = jo["t_id"].ToString();
304 | string transTimestamp = jo["t_ts"].ToString();
305 | string orgTransId = jo["orgt_id"].ToString();
306 | string orgTransTimestamp = jo["orgt_ts"].ToString();
307 | string base64EncodedReceipt = jo["t_receipt"].ToString();
308 | // You can also add the bundle identifier.
309 | requestToVerifyReceipt(productId, transId, base64EncodedReceipt, userId, transTimestamp, orgTransId, orgTransTimestamp);
310 | } catch (System.Exception e) {
311 | LogManager.Log (e.ToString (), LogType.Fatal);
312 | }
313 | }
314 |
315 | private void requestToVerifyReceipt(string productId, string transId, string base64EncodedReceipt,
316 | string userId, string transTimestamp, string orgTransId, string orgTransTimestamp) {
317 |
318 | // The URL for receipt verification.
319 | // Sandbox: "https://sandbox.itunes.apple.com/verifyReceipt"
320 | // Production: "https://buy.itunes.apple.com/verifyReceipt"
321 |
322 | // Finally, you call this method to complete the transaction.
323 | // finishTransaction(transactionId); finishTransaction(orgTransactionId);
324 |
325 | // Recommended reference links:
326 | // https://itenfay.github.io/2016/10/16/in-app-purchase-complete-programming-guide-for-iOS/
327 | // https://itenfay.github.io/2016/10/12/how-to-easily-complete-in-app-purchase-configuration-for-iOS/
328 | // https://www.jianshu.com/p/de030cd6e4a3
329 | // https://www.jianshu.com/p/1875e0c7ac5d
330 |
331 | // Performs http request or builds tcp/udp connection.
332 | }
333 | ```
334 |
335 | Finally, after the receipt is verified, you need to complete the corresponding transaction.
336 |
337 | ```
338 | public void finishTransaction(string transactionId, string originalTransactionId)
339 | {
340 | if (Application.platform != RuntimePlatform.OSXEditor) {
341 | LogManager.Log("finishTransaction", LogType.Normal);
342 | #if !UNITY_EDITOR && UNITY_IOS
343 | DYFFinishTransaction(transactionId, originalTransactionId);
344 | #endif
345 | }
346 | }
347 |
348 | public void finishTransaction_(string transactionId)
349 | {
350 | if (Application.platform != RuntimePlatform.OSXEditor) {
351 | LogManager.Log("finishTransaction", LogType.Normal);
352 | #if !UNITY_EDITOR && UNITY_IOS
353 | DYFFinishTransaction_(transactionId);
354 | #endif
355 | }
356 | }
357 | ```
358 |
359 | - Queries those incompleted transactions.
360 |
361 | If there are the receipts in keychain and the receipt verification has not been completed, you need to query them out, and then report them one by one until the transaction, and then delete the corresponding record in the keychain.
362 |
363 | ```
364 | public void queryIncompletedTransactions()
365 | {
366 | if (Application.platform != RuntimePlatform.OSXEditor) {
367 | LogManager.Log("queryIncompletedTransactions", LogType.Normal);
368 | #if !UNITY_EDITOR && UNITY_IOS
369 | DYFQueryIncompletedTransactions();
370 | #endif
371 | }
372 | }
373 | ```
374 |
375 |
376 | ## Recommended Reference Links
377 |
378 | - [in-app-purchase-complete-programming-guide-for-iOS](https://itenfay.github.io/2016/10/16/in-app-purchase-complete-programming-guide-for-iOS/)
379 | - [how-to-easily-complete-in-app-purchase-configuration-for-iOS](https://itenfay.github.io/2016/10/12/how-to-easily-complete-in-app-purchase-configuration-for-iOS/)
380 | - [https://www.jianshu.com/p/de030cd6e4a3](https://www.jianshu.com/p/de030cd6e4a3)
381 | - [https://www.jianshu.com/p/1875e0c7ac5d](https://www.jianshu.com/p/1875e0c7ac5d)
382 |
383 |
384 | ## Requirements
385 |
386 | `Unity_iOS_InAppPurchase` requires `iOS 7.0` or above and `ARC`.
387 |
388 |
389 | ## Feedback is welcome
390 |
391 | If you notice any issue to create an issue. I will be happy to help you.
392 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **中文版** | [English Version](README-en.md)
2 |
3 |
4 | ## Unity-iOS-InAppPurchase
5 |
6 | Unity实现苹果iOS的应用内购买。
7 |
8 | [](LICENSE)
9 |
10 |
11 | ## QQ群 (ID:614799921)
12 |
13 |
14 |

15 |
16 |
17 |
18 | ## 使用
19 |
20 | `unity-iap`的目录结构如下:
21 |
22 | - **Objective-C**
23 |
24 | | Dir | file |
25 | | :------------------------: | :----------------: |
26 | | objc(StoreManager External)| UnityIAPConnector.h/.mm |
27 | | StoreManager | DYFStoreManager.h/.mm |
28 | | External(Optional) | DYFLoadingView.h/.m DYFIndefiniteAnimatedSpinner.h/.m NSObject+DYFAdd.h/.m UIView+DYFAdd.h/.m |
29 |
30 | - **Unity**
31 |
32 | | Dir | file |
33 | | :----------------: | :----------------: |
34 | | unity | UnityIAPManager.cs |
35 |
36 | > **Note: Unity需要在适当的时候显示/隐藏加载面板或显示提示消息。**
37 |
38 | ### 1、添加 Objective-C 所需要的文件
39 |
40 | 在 Unity 工程中添加 Objective-C 所需要的文件。
41 |
42 | ### 2、添加 cs 脚本
43 |
44 | 在 Unity 工程中添加 iOS 内购实现所需要的 cs 脚本。
45 |
46 | ### 3、添加 DYFStoreKit 目录文件
47 |
48 | 使用 `pod 'DYFStoreKit'` 添加最新版本的 iOS 内购库。
49 |
50 | 或者
51 |
52 | 克隆 `DYFStoreKit`(`git clone https://github.com/itenfay/DYFStoreKit.git`)到本地目录。
53 |
54 | ### 4、添加交易监听和其他
55 |
56 | 在 UnityAppController.mm 中添加头文件 `#import "DYFStoreManager.h"`
57 |
58 | - 遵守协议
59 |
60 | ```
61 | @interface UnityAppController()
62 |
63 | @end
64 | ```
65 |
66 | - 添加观察者、设置代理和数据持久
67 |
68 | 只要在方法返回值前添加以下一段代码,其他代码不要改变。
69 |
70 | ```
71 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
72 | {
73 | [self initIAPSDK];
74 | return YES;
75 | }
76 |
77 | - (void)initIAPSDK
78 | {
79 | [DYFStoreManager.shared addStoreObserver];
80 |
81 | // Adds an observer that responds to updated transactions to the payment queue.
82 | // If an application quits when transactions are still being processed, those transactions are not lost. The next time the application launches, the payment queue will resume processing the transactions. Your application should always expect to be notified of completed transactions.
83 | // If more than one transaction observer is attached to the payment queue, no guarantees are made as to the order they will be called in. It is recommended that you use a single observer to process and finish the transaction.
84 | [DYFStore.defaultStore addPaymentTransactionObserver];
85 |
86 | // Sets the delegate processes the purchase which was initiated by user from the App Store.
87 | DYFStore.defaultStore.delegate = self;
88 | }
89 | ```
90 |
91 | - 你可以处理用户从App Store发起的购买 (iOS 11.0+)
92 |
93 | ```
94 | // Processes the purchase which was initiated by user from the App Store.
95 | - (void)didReceiveAppStorePurchaseRequest:(SKPaymentQueue *)queue payment:(SKPayment *)payment forProduct:(SKProduct *)product
96 | {
97 | if (![DYFStore canMakePayments]) {
98 | // Tips: Your device is not able or allowed to make payments!
99 | return;
100 | }
101 |
102 | // Get account name from your own user system.
103 | NSString *accountName = @"Handsome Jon";
104 | // This algorithm is negotiated with server developer.
105 | NSString *userIdentifier = DYFStore_supplySHA256(accountName);
106 | DYFStoreLog(@"userIdentifier: %@", userIdentifier);
107 |
108 | [DYFStoreManager.shared addPayment:product.productIdentifier userIdentifier:userIdentifier];
109 | }
110 | ```
111 |
112 | ### 5、注意事项
113 |
114 | - 初始化 Unity 回调对象和函数
115 |
116 | ```
117 | public void initUnityMsgCallback(string gameObject, string func)
118 | {
119 | LogManager.Log("initUnityMsgCallback=" + gameObject + "," + func);
120 | #if !UNITY_EDITOR && UNITY_IOS
121 | DYFInitUnityMsgCallback(gameObject, func);
122 | #endif
123 | }
124 | ```
125 |
126 | - 单个商品购买
127 |
128 | 如果你的应用含有购买的 UI 界面和商品展示信息,用户就只需要选择购买。 缺点就是程序要做当地价格匹配。
129 |
130 | ```
131 | public void retrieveProduct(string productId)
132 | {
133 | if (Application.platform != RuntimePlatform.OSXEditor) {
134 | LogManager.Log("retrieveProduct=" + productId);
135 | #if !UNITY_EDITOR && UNITY_IOS
136 | // Tips: show loading panel.
137 | DYFRetrieveProductFromAppStore(productId);
138 | #endif
139 | }
140 | }
141 | ```
142 |
143 | 当程序得到成功的回调时,就可以解析数据进行添加付款了。其他情况程序要对用户弹框提示。
144 |
145 | ```
146 | case (int)CallbackType.Action_GetProductSuccessfully:
147 | {
148 | LogManager.Log ("CallbackType.Action_GetProductSuccessfully");
149 | string productId = (string)json["msg_data"]["p_id"];
150 | // You get this from your user system when you need it.
151 | string userId = null;
152 | addPayment(productId, userId);
153 | break;
154 | }
155 | ````
156 |
157 | - 请求多个商品并展示购买的 UI 界面
158 |
159 | 一次请求多个商品,通过工具获得商品的本地化信息。
160 |
161 | ```
162 | // You can either return the value or get a set of product identifiers from your server.
163 | private JArray getJArrayOfProductIds()
164 | {
165 | JArray a = new JArray();
166 | foreach (var obj in LaunchMng.IAPDict) {
167 | a.Add(obj.Value);
168 | }
169 | return a;
170 | }
171 |
172 | // Converts JArray to json string.
173 | private string getJsonOfProductIds()
174 | {
175 | JArray a = getJArrayOfProductIds();
176 | string json = JsonConvert.SerializeObject(a);
177 | return json;
178 | }
179 |
180 | public void retrieveProducts()
181 | {
182 | if (Application.platform != RuntimePlatform.OSXEditor) {
183 | string jsonOfProductIds = getJsonOfProductIds()
184 | LogManager.Log("retrieveProducts=" + jsonOfProductIds);
185 | #if !UNITY_EDITOR && UNITY_IOS
186 | // Tips: show loading panel.
187 | DYFRetrieveProductsFromAppStore(jsonOfProductIds);
188 | #endif
189 | }
190 | }
191 | ```
192 |
193 | 当程序得到成功的回调时,就可以解析数据进行展示购买的 UI 界面。其他情况程序要对用户弹框提示。
194 |
195 |
196 | ```
197 | case (int)CallbackType.Action_GetProductsSuccessfully:
198 | {
199 | LogManager.Log ("CallbackType.Action_GetProductsSuccessfully");
200 | JArray arr = JArray.Parse(json["msg_data"].ToString());
201 | parseProductList(arr);
202 | break;
203 | }
204 |
205 | private void parseProductList(JArray jarr)
206 | {
207 | try {
208 | LogManager.Log ("parseProductList... jarr: " + jarr.ToString());
209 | for(int i = 0; i < jarr.Count; i++) {
210 | JObject jo = JObject.Parse(jarr[i].ToString());
211 | string productId = jo["p_id"].ToString();
212 | string title = jo["p_title"].ToString();
213 | string price = jo["p_price"].ToString();
214 | string localizedPrice = jo["p_localized_price"].ToString();
215 | string localizedDesc = jo["p_localized_desc"].ToString();
216 |
217 | LogManager.Log ("ProductList..." + i.ToString() + " productId: " + productId +
218 | "; title: " + title + "; price: " + price + "; localizedPrice: " +
219 | localizedPrice + "; localizedDesc: " + localizedDesc);
220 | }
221 | // Call displayStorePanel(), The parameters need to be defined by you.
222 | // Waiting for the user to choose to buy goods, then call addPayment(...)
223 | } catch (System.Exception e) {
224 | LogManager.Log (e.ToString (), LogType.Fatal);
225 | }
226 | }
227 |
228 | private void displayStorePanel()
229 | {
230 | // After getting the products, then the store panel is displayed.
231 | }
232 | ```
233 |
234 | 用户选择购买商品,用户 id 可根据需要进行设置。
235 |
236 | ```
237 | public void addPayment(string productId, string userId)
238 | {
239 | if (Application.platform != RuntimePlatform.OSXEditor) {
240 | LogManager.Log("addPayment=" + productId + "," + userId);
241 | #if !UNITY_EDITOR && UNITY_IOS
242 | // Tips: show loading panel.
243 | DYFAddPayment(productId, userId);
244 | #endif
245 | }
246 | }
247 | ```
248 |
249 | - 恢复已经完成的交易,用户 id 可选
250 |
251 | ```
252 | public void restoreTransactions(string userId)
253 | {
254 | if (Application.platform != RuntimePlatform.OSXEditor) {
255 | LogManager.Log("DYFRestoreTransactions=", userId);
256 | #if !UNITY_EDITOR && UNITY_IOS
257 | // Tips: show loading panel.
258 | DYFRestoreTransactions(userId);
259 | #endif
260 | LogManager.Log("Store start restoring completed transactions...", LogType.Normal);
261 | }
262 | }
263 | ```
264 |
265 | - 刷新票据
266 |
267 | 如果票据无效或丢失,就要刷新App Store票据。
268 |
269 | ```
270 | case(int)CallbackType.Action_RefreshReceipt:
271 | {
272 | LogManager.Log ("CallbackType.Action_RefreshReceipt");
273 | // Tips: The receipt needs to be refreshed.
274 | string desc = (string)json["msg_data"]["m_desc"];
275 | LogManager.Log("err_desc=", desc);
276 | refreshReceipt()
277 | break;
278 | }
279 | ```
280 |
281 | ```
282 | public void refreshReceipt()
283 | {
284 | if (Application.platform != RuntimePlatform.OSXEditor) {
285 | #if !UNITY_EDITOR && UNITY_IOS
286 | // Tips: show loading panel.
287 | DYFRefreshReceipt();
288 | #endif
289 | LogManager.Log("Store start refreshing receipt...", LogType.Normal);
290 | }
291 | }
292 | ```
293 |
294 | - 票据验证
295 |
296 | ```
297 | private void verifyReceipt(JObject jo)
298 | {
299 | try {
300 | LogManager.Log ("verifyReceipt... jo: " + jo.ToString());
301 | int state = int.Parse(jo["t_state"].ToString);
302 | string productId = jo["p_id"].ToString();
303 | string userId = jo["u_id"].ToString();
304 | string transId = jo["t_id"].ToString();
305 | string transTimestamp = jo["t_ts"].ToString();
306 | string orgTransId = jo["orgt_id"].ToString();
307 | string orgTransTimestamp = jo["orgt_ts"].ToString();
308 | string base64EncodedReceipt = jo["t_receipt"].ToString();
309 | // You can also add the bundle identifier.
310 | requestToVerifyReceipt(productId, transId, base64EncodedReceipt, userId, transTimestamp, orgTransId, orgTransTimestamp);
311 | } catch (System.Exception e) {
312 | LogManager.Log (e.ToString (), LogType.Fatal);
313 | }
314 | }
315 |
316 | private void requestToVerifyReceipt(string productId, string transId, string base64EncodedReceipt,
317 | string userId, string transTimestamp, string orgTransId, string orgTransTimestamp) {
318 |
319 | // The URL for receipt verification.
320 | // Sandbox: "https://sandbox.itunes.apple.com/verifyReceipt"
321 | // Production: "https://buy.itunes.apple.com/verifyReceipt"
322 |
323 | // Finally, you call this method to complete the transaction.
324 | // finishTransaction(transactionId); finishTransaction(orgTransactionId);
325 |
326 | // Recommended reference links:
327 | // https://itenfay.github.io/2016/10/16/in-app-purchase-complete-programming-guide-for-iOS/
328 | // https://itenfay.github.io/2016/10/12/how-to-easily-complete-in-app-purchase-configuration-for-iOS/
329 | // https://www.jianshu.com/p/de030cd6e4a3
330 | // https://www.jianshu.com/p/1875e0c7ac5d
331 |
332 | // Performs http request or builds tcp/udp connection.
333 | }
334 | ```
335 |
336 | 最后,在票据验证通过后,你要完成相应的交易。
337 |
338 | ```
339 | public void finishTransaction(string transactionId, string originalTransactionId)
340 | {
341 | if (Application.platform != RuntimePlatform.OSXEditor) {
342 | LogManager.Log("finishTransaction", LogType.Normal);
343 | #if !UNITY_EDITOR && UNITY_IOS
344 | DYFFinishTransaction(transactionId, originalTransactionId);
345 | #endif
346 | }
347 | }
348 |
349 | public void finishTransaction_(string transactionId)
350 | {
351 | if (Application.platform != RuntimePlatform.OSXEditor) {
352 | LogManager.Log("finishTransaction", LogType.Normal);
353 | #if !UNITY_EDITOR && UNITY_IOS
354 | DYFFinishTransaction_(transactionId);
355 | #endif
356 | }
357 | }
358 | ```
359 |
360 | - 查询未完成的交易
361 |
362 | 如果票据存在keychain并且没有完成验证,那么你需要查询出来,然后一一进行上报,直至交易,进而删除 keychain 中相应的记录。
363 |
364 | ```
365 | public void queryIncompletedTransactions()
366 | {
367 | if (Application.platform != RuntimePlatform.OSXEditor) {
368 | LogManager.Log("queryIncompletedTransactions", LogType.Normal);
369 | #if !UNITY_EDITOR && UNITY_IOS
370 | DYFQueryIncompletedTransactions();
371 | #endif
372 | }
373 | }
374 | ```
375 |
376 |
377 | ## 推荐参考链接
378 |
379 | - [in-app-purchase-complete-programming-guide-for-iOS](https://itenfay.github.io/2016/10/16/in-app-purchase-complete-programming-guide-for-iOS/)
380 | - [how-to-easily-complete-in-app-purchase-configuration-for-iOS](https://itenfay.github.io/2016/10/12/how-to-easily-complete-in-app-purchase-configuration-for-iOS/)
381 | - [https://www.jianshu.com/p/de030cd6e4a3](https://www.jianshu.com/p/de030cd6e4a3)
382 | - [https://www.jianshu.com/p/1875e0c7ac5d](https://www.jianshu.com/p/1875e0c7ac5d)
383 |
384 |
385 | ## 要求
386 |
387 | `Unity_iOS_InAppPurchase`需要`iOS 7.0`或更高版本和ARC。
388 |
389 |
390 | ## 欢迎反馈
391 |
392 | 如果您发现任何问题,请创建问题。我很乐意帮助你。
393 |
--------------------------------------------------------------------------------
/unity-iap/objc/External/DYFIndefiniteAnimatedSpinner.h:
--------------------------------------------------------------------------------
1 | //
2 | // DYFIndefiniteAnimatedSpinner.h
3 | //
4 | // Created by Tenfay on 2014/11/4. ( https://github.com/itenfay/DYFStoreKit )
5 | // Copyright © 2014 Tenfay. All rights reserved.
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in
15 | // all copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | // THE SOFTWARE.
24 | //
25 |
26 | #import
27 |
28 | @interface DYFIndefiniteAnimatedSpinner : UIView
29 |
30 | /**
31 | Property indicating whether the view is currently animating.
32 | */
33 | @property (nonatomic, readonly) BOOL isAnimating;
34 |
35 | /**
36 | Sets whether the view is hidden when not animating.
37 | */
38 | @property (nonatomic) BOOL hidesWhenStopped;
39 |
40 | /**
41 | Specifies the timing function to use for the control's animation. Defaults to kCAMediaTimingFunctionEaseInEaseOut.
42 | */
43 | @property (nonatomic, strong) CAMediaTimingFunction *timingFunction;
44 |
45 | /**
46 | Sets the line width of the spinner's circle.
47 | */
48 | @property (nonatomic) CGFloat lineWidth;
49 |
50 | /**
51 | Sets the line color of the spinner's circle.
52 | */
53 | @property (nonatomic, strong) UIColor *lineColor;
54 |
55 | /**
56 | Starts animation of the spinner.
57 | */
58 | - (void)startAnimating;
59 |
60 | /**
61 | Stops animation of the spinnner.
62 | */
63 | - (void)stopAnimating;
64 |
65 | @end
66 |
--------------------------------------------------------------------------------
/unity-iap/objc/External/DYFIndefiniteAnimatedSpinner.m:
--------------------------------------------------------------------------------
1 | //
2 | // DYFIndefiniteAnimatedSpinner.h
3 | //
4 | // Created by Tenfay on 2014/11/4. ( https://github.com/itenfay/DYFStoreKit )
5 | // Copyright © 2014 Tenfay. All rights reserved.
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in
15 | // all copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | // THE SOFTWARE.
24 | //
25 |
26 | #import "DYFIndefiniteAnimatedSpinner.h"
27 |
28 | /** Animation Key */
29 | NSString *const DYFSpinnerStrokeAnimationKey = @"spinner.animkey.stroke";
30 | NSString *const DYFSpinnerRotationAnimationKey = @"spinner.animkey.rotation";
31 |
32 | @interface DYFIndefiniteAnimatedSpinner ()
33 |
34 | /** A layer that draws a arc progress in its coordinate space. */
35 | @property (nonatomic, readonly) CAShapeLayer *progressLayer;
36 |
37 | @property (nonatomic, readwrite) BOOL isAnimating;
38 |
39 | @end
40 |
41 | @implementation DYFIndefiniteAnimatedSpinner
42 |
43 | @synthesize progressLayer = _progressLayer;
44 |
45 | - (instancetype)initWithFrame:(CGRect)frame
46 | {
47 | if (self = [super initWithFrame:frame]) {
48 | [self setup];
49 | }
50 | return self;
51 | }
52 |
53 | - (instancetype)initWithCoder:(NSCoder *)coder
54 | {
55 | self = [super initWithCoder:coder];
56 | if (self) {
57 | // Supports an Interface Builder archive, or nib file.
58 | }
59 | return self;
60 | }
61 |
62 | - (void)awakeFromNib
63 | {
64 | [super awakeFromNib];
65 | [self setup];
66 | }
67 |
68 | - (void)setup
69 | {
70 | [self.layer addSublayer:self.progressLayer];
71 | self.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
72 |
73 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(resetAnimations) name:UIApplicationDidBecomeActiveNotification object:nil];
74 | }
75 |
76 | - (BOOL)isAnimating
77 | {
78 | return _isAnimating;
79 | }
80 |
81 | - (void)setHidesWhenStopped:(BOOL)hidesWhenStopped
82 | {
83 | _hidesWhenStopped = hidesWhenStopped;
84 | self.hidden = !self.isAnimating && hidesWhenStopped;
85 | }
86 |
87 | - (CGFloat)lineWidth
88 | {
89 | return self.progressLayer.lineWidth;
90 | }
91 |
92 | - (void)setLineWidth:(CGFloat)lineWidth
93 | {
94 | self.progressLayer.lineWidth = lineWidth;
95 | [self updatePath];
96 | }
97 |
98 | - (UIColor *)lineColor
99 | {
100 | CGColorRef color = self.progressLayer.strokeColor;
101 |
102 | if (color) {
103 | return [UIColor colorWithCGColor:color];
104 | }
105 |
106 | return nil;
107 | }
108 |
109 | - (void)setLineColor:(UIColor *)lineColor
110 | {
111 | self.progressLayer.strokeColor = lineColor.CGColor;
112 | }
113 |
114 | #pragma mark - Lazy Load
115 |
116 | - (CAShapeLayer *)progressLayer
117 | {
118 | if (!_progressLayer) {
119 | _progressLayer = [CAShapeLayer layer];
120 | _progressLayer.strokeColor = nil;
121 | _progressLayer.fillColor = nil;
122 | _progressLayer.lineWidth = 1.f;
123 | }
124 | return _progressLayer;
125 | }
126 |
127 | - (void)startAnimating
128 | {
129 | if (self.isAnimating) {
130 | return;
131 | }
132 |
133 | self.isAnimating = YES;
134 | [self addLayerAnimations];
135 | self.hidden = NO;
136 | }
137 |
138 | - (void)stopAnimating
139 | {
140 | if (!self.isAnimating) {
141 | return;
142 | }
143 |
144 | self.isAnimating = NO;
145 |
146 | [self.progressLayer removeAnimationForKey:DYFSpinnerRotationAnimationKey];
147 | [self.progressLayer removeAnimationForKey:DYFSpinnerStrokeAnimationKey];
148 |
149 | if (self.hidesWhenStopped) {
150 | self.hidden = YES;
151 | }
152 | }
153 |
154 | - (void)addLayerAnimations
155 | {
156 | CABasicAnimation *animation = [CABasicAnimation animation];
157 | animation.keyPath = @"transform.rotation";
158 | animation.duration = 2.f;
159 | animation.fromValue = @(0.f);
160 | animation.toValue = @(2 * M_PI);
161 | animation.repeatCount = INFINITY;
162 | [self.progressLayer addAnimation:animation forKey:DYFSpinnerRotationAnimationKey];
163 |
164 | CABasicAnimation *headAnimation = [CABasicAnimation animation];
165 | headAnimation.keyPath = @"strokeStart";
166 | headAnimation.duration = 1.f;
167 | headAnimation.fromValue = @(0.f);
168 | headAnimation.toValue = @(0.25f);
169 | headAnimation.timingFunction = self.timingFunction;
170 |
171 | CABasicAnimation *tailAnimation = [CABasicAnimation animation];
172 | tailAnimation.keyPath = @"strokeEnd";
173 | tailAnimation.duration = 1.f;
174 | tailAnimation.fromValue = @(0.f);
175 | tailAnimation.toValue = @(1.f);
176 | tailAnimation.timingFunction = self.timingFunction;
177 |
178 | CABasicAnimation *endHeadAnimation = [CABasicAnimation animation];
179 | endHeadAnimation.keyPath = @"strokeStart";
180 | endHeadAnimation.beginTime = 1.f;
181 | endHeadAnimation.duration = 0.5f;
182 | endHeadAnimation.fromValue = @(0.25f);
183 | endHeadAnimation.toValue = @(1.f);
184 | endHeadAnimation.timingFunction = self.timingFunction;
185 |
186 | CABasicAnimation *endTailAnimation = [CABasicAnimation animation];
187 | endTailAnimation.keyPath = @"strokeEnd";
188 | endTailAnimation.beginTime = 1.f;
189 | endTailAnimation.duration = 0.5f;
190 | endTailAnimation.fromValue = @(1.f);
191 | endTailAnimation.toValue = @(1.f);
192 | endTailAnimation.timingFunction = self.timingFunction;
193 |
194 | CAAnimationGroup *animGroup = [CAAnimationGroup animation];
195 | [animGroup setDuration:1.5f];
196 | [animGroup setAnimations:@[headAnimation,
197 | tailAnimation,
198 | endHeadAnimation,
199 | endTailAnimation]];
200 | animGroup.repeatCount = INFINITY;
201 | [self.progressLayer addAnimation:animGroup forKey:DYFSpinnerStrokeAnimationKey];
202 | }
203 |
204 | - (void)resetAnimations
205 | {
206 | if (self.isAnimating) {
207 | [self stopAnimating];
208 | [self startAnimating];
209 | }
210 | }
211 |
212 | - (void)layoutSubviews
213 | {
214 | [super layoutSubviews];
215 |
216 | CGFloat sW = CGRectGetWidth(self.bounds);
217 | CGFloat sH = CGRectGetHeight(self.bounds);
218 | self.progressLayer.frame = CGRectMake(0, 0, sW, sH);
219 |
220 | [self updatePath];
221 | }
222 |
223 | #pragma mark - Private
224 |
225 | - (void)updatePath
226 | {
227 | CGPoint center = CGPointMake(CGRectGetMidX(self.bounds),
228 | CGRectGetMidY(self.bounds));
229 | CGFloat radius = MIN(CGRectGetWidth(self.bounds)/2, CGRectGetHeight(self.bounds)/2) - self.lineWidth/2;
230 | CGFloat startAngle = (CGFloat)(0);
231 | CGFloat endAngle = (CGFloat)(2*M_PI);
232 |
233 | UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
234 | self.progressLayer.path = path.CGPath;
235 |
236 | self.progressLayer.strokeStart = 0.f;
237 | self.progressLayer.strokeEnd = 0.f;
238 | }
239 |
240 | - (void)executeWhenReleasing
241 | {
242 | [self stopAnimating];
243 | [NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
244 | }
245 |
246 | - (void)dealloc
247 | {
248 | #if DEBUG
249 | NSLog(@"%s", __func__);
250 | #endif
251 | [self executeWhenReleasing];
252 | }
253 |
254 | @end
255 |
--------------------------------------------------------------------------------
/unity-iap/objc/External/DYFLoadingView.h:
--------------------------------------------------------------------------------
1 | //
2 | // DYFLoadingView.h
3 | //
4 | // Created by Tenfay on 2014/11/4. ( https://github.com/itenfay/DYFStoreKit )
5 | // Copyright © 2014 Tenfay. All rights reserved.
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in
15 | // all copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | // THE SOFTWARE.
24 | //
25 |
26 | #import
27 | #import "UIView+DYFAdd.h"
28 |
29 | /** Creates and returns a color object using the specified opacity and RGB component values.
30 |
31 | @param r The red value of the color object, specified as a value from 0.0 to 255.0.
32 | @param g The green value of the color object, specified as a value from 0.0 to 255.0.
33 | @param b The blue value of the color object, specified as a value from 0.0 to 255.0.
34 | @param alp The opacity value of the color object, specified as a value from 0.0 to 1.0.
35 | @return The color object. The color information represented by this object is in an RGB colorspace.
36 | */
37 | #define COLOR_RGBA(r, g, b, alp) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:(alp)]
38 |
39 | /** Creates and returns a color object using the specified opacity and RGB component values.
40 |
41 | @param r The red value of the color object, specified as a value from 0.0 to 255.0.
42 | @param g The green value of the color object, specified as a value from 0.0 to 255.0.
43 | @param b The blue value of the color object, specified as a value from 0.0 to 255.0.
44 | @return The color object. The color information represented by this object is in an RGB colorspace.
45 | */
46 | #define COLOR_RGB(r, g, b) COLOR_RGBA(r, g, b, 1.0)
47 |
48 | /** Returns the width of the screen for the device.
49 |
50 | @return The width of the screen for the device.
51 | */
52 | #define SCREEN_W UIScreen.mainScreen.bounds.size.width
53 |
54 | /** Returns the height of the screen for the device.
55 |
56 | @return The height of the screen for the device.
57 | */
58 | #define SCREEN_H UIScreen.mainScreen.bounds.size.height
59 |
60 | /** The block is called when the text for label is set.
61 |
62 | @param text The text for label.
63 | */
64 | typedef void (^LoadingViewConfigurationBlock)(NSString *text);
65 |
66 | @interface DYFLoadingView : UIView
67 |
68 | /** The color to set the background color of the content view.
69 | */
70 | @property (nonatomic, strong) UIColor *color;
71 |
72 | /** The color to set the line color of the indicator.
73 | */
74 | @property (nonatomic, strong) UIColor *indicatorColor;
75 |
76 | /** The color to set the text color of the text label.
77 | */
78 | @property (nonatomic, strong) UIColor *textColor;
79 |
80 | /** Disable this method to instantiate an object.
81 | */
82 | - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
83 |
84 | /** It will be displayed on the screen with the text.
85 |
86 | @return A block with the text.
87 | */
88 | - (LoadingViewConfigurationBlock)show;
89 |
90 | /** Hides from its own superview.
91 | */
92 | - (void)hide;
93 |
94 | @end
95 |
--------------------------------------------------------------------------------
/unity-iap/objc/External/DYFLoadingView.m:
--------------------------------------------------------------------------------
1 | //
2 | // DYFLoadingView.m
3 | //
4 | // Created by Tenfay on 2014/11/4. ( https://github.com/itenfay/DYFStoreKit )
5 | // Copyright © 2014 Tenfay. All rights reserved.
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in
15 | // all copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | // THE SOFTWARE.
24 | //
25 |
26 | #import "DYFLoadingView.h"
27 | #import "DYFIndefiniteAnimatedSpinner.h"
28 |
29 | @interface DYFLoadingView ()
30 |
31 | @property (nonatomic, strong) UIView *maskPanel;
32 | @property (nonatomic, strong) UIView *contentView;
33 | @property (nonatomic, strong) DYFIndefiniteAnimatedSpinner *indicator;
34 | @property (nonatomic, strong) UILabel *textLabel;
35 |
36 | @end
37 |
38 | @implementation DYFLoadingView
39 |
40 | /// It is used to act as background mask panel.
41 | - (UIView *)maskPanel
42 | {
43 | if (!_maskPanel) {
44 | _maskPanel = [[UIView alloc] init];
45 | _maskPanel.backgroundColor = COLOR_RGBA(20, 20, 20, 0.5);
46 | }
47 | return _maskPanel;
48 | }
49 |
50 | /// It is used to render the content.
51 | - (UIView *)contentView
52 | {
53 | if (!_contentView) {
54 | _contentView = [[UIView alloc] init];
55 | _contentView.backgroundColor = COLOR_RGB(255, 255, 255);
56 | }
57 | return _contentView;
58 | }
59 |
60 | /// The spinner is used to provide an indefinite animation.
61 | - (DYFIndefiniteAnimatedSpinner *)indicator
62 | {
63 | if (!_indicator) {
64 | _indicator = [[DYFIndefiniteAnimatedSpinner alloc] init];
65 | _indicator.backgroundColor = UIColor.clearColor;
66 | _indicator.lineColor = COLOR_RGB(100, 100, 100);
67 | }
68 | return _indicator;
69 | }
70 |
71 | /// It is used to show the text.
72 | - (UILabel *)textLabel
73 | {
74 | if (!_textLabel) {
75 | _textLabel = [[UILabel alloc] init];
76 | _textLabel.backgroundColor = UIColor.clearColor;
77 | _textLabel.textColor = COLOR_RGB(60, 60, 60);
78 | }
79 | return _textLabel;
80 | }
81 |
82 | /// Returns the current window of the app.
83 | - (UIWindow *)appWindow
84 | {
85 | UIApplication *sharedApp = UIApplication.sharedApplication;
86 | return sharedApp.keyWindow ?: sharedApp.windows[0];
87 | }
88 |
89 | /// Returns the background color of the content view.
90 | - (UIColor *)color
91 | {
92 | return self.contentView.backgroundColor;
93 | }
94 |
95 | /// The color to set the background color of the content view.
96 | /// @param color The color you will set.
97 | - (void)setColor:(UIColor *)color
98 | {
99 | self.contentView.backgroundColor = color;
100 | }
101 |
102 | /// Returns the line color of the indicator.
103 | - (UIColor *)indicatorColor
104 | {
105 | return self.indicator.lineColor;
106 | }
107 |
108 | /// The color to set the line color of the indicator.
109 | /// @param indicatorColor The indicator color you will set.
110 | - (void)setIndicatorColor:(UIColor *)indicatorColor
111 | {
112 | self.indicator.lineColor = indicatorColor;
113 | }
114 |
115 | /// Returns the text color of the text label.
116 | - (UIColor *)textColor
117 | {
118 | return self.textLabel.textColor;
119 | }
120 |
121 | /// The color to set the text color of the text label.
122 | /// @param textColor The text color you will set.
123 | - (void)setTextColor:(UIColor *)textColor
124 | {
125 | self.textLabel.textColor = textColor;
126 | }
127 |
128 | /// Returns an object initialized from data in a given unarchiver.
129 | /// @param coder An unarchiver object.
130 | - (instancetype)initWithCoder:(NSCoder *)coder
131 | {
132 | self = [super initWithCoder:coder];
133 | if (self) {}
134 | return self;
135 | }
136 |
137 | - (void)awakeFromNib
138 | {
139 | [super awakeFromNib];
140 | // Prepares the receiver for service after it has been loaded
141 | // from an Interface Builder archive, or nib file.
142 | }
143 |
144 | /// It will be displayed on the screen with the text.
145 | - (LoadingViewConfigurationBlock)show
146 | {
147 | LoadingViewConfigurationBlock block;
148 | block = ^(NSString *text) {
149 | [self configure:text];
150 | [self loadView];
151 | [self beginAnimating];
152 | };
153 | return block;
154 | }
155 |
156 | /// Hides from its own superview.
157 | - (void)hide
158 | {
159 | UIViewAnimationOptions opts = UIViewAnimationOptionCurveEaseInOut;
160 | [UIView animateWithDuration:0.3 delay:1.0 options:opts animations:^{
161 | self.alpha = 0.f;
162 | } completion:^(BOOL finished) {
163 | [self.indicator stopAnimating];
164 | [self removeAllViews];
165 | [self safetyRelease];
166 | }];
167 | }
168 |
169 | /// Removes all views at the end of the hidden animation.
170 | - (void)removeAllViews
171 | {
172 | for (UIView *view in self.subviews) {
173 | [view removeFromSuperview];
174 | }
175 | [self removeFromSuperview];
176 | }
177 |
178 | /// When the view is hidden, the child widget object should be released safely.
179 | - (void)safetyRelease
180 | {
181 | if (_maskPanel) { _maskPanel = nil; }
182 | if (_contentView) { _contentView = nil; }
183 | if (_indicator) { _indicator = nil; }
184 | if (_textLabel) { _textLabel = nil; }
185 | }
186 |
187 | /// Configures properties for the widget used.
188 | - (void)configure:(NSString *)text
189 | {
190 | self.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin |
191 | UIViewAutoresizingFlexibleWidth |
192 | UIViewAutoresizingFlexibleTopMargin |
193 | UIViewAutoresizingFlexibleHeight);
194 |
195 | self.maskPanel.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin |
196 | UIViewAutoresizingFlexibleWidth |
197 | UIViewAutoresizingFlexibleTopMargin |
198 | UIViewAutoresizingFlexibleHeight);
199 |
200 | CGFloat cw = 200.f;
201 | self.contentView.frame = CGRectMake(0, 0, cw, 0.6*cw);
202 | self.contentView.setCorner(UIRectCornerAllCorners, 10.f);
203 |
204 | CGFloat offset = 10.f;
205 | CGFloat iw = 60.f;
206 | CGFloat ix = cw/2 - iw/2;
207 | CGFloat iy = 1.5*offset;
208 | self.indicator.frame = CGRectMake(ix, iy, iw, iw);
209 | self.indicator.lineWidth = 2.0;
210 |
211 | CGFloat lh = 20.f;
212 | self.textLabel.center = CGPointMake(cw/2, 0.6*cw - lh/2 - 1.5*offset);
213 | self.textLabel.bounds = CGRectMake(0, 0, cw - 2*offset, lh);
214 | self.textLabel.text = text;
215 | self.textLabel.font = [UIFont boldSystemFontOfSize:16.f];
216 | self.textLabel.textAlignment = NSTextAlignmentCenter;
217 | self.textLabel.numberOfLines = 1;
218 | }
219 |
220 | /// Addds the subviews to its corresponding superview.
221 | - (void)loadView
222 | {
223 | UIViewController *vc = self.appCurrentViewController;
224 | if (vc != nil) {
225 | [vc.view addSubview:self];
226 | [vc.view bringSubviewToFront:self];
227 | } else {
228 | UIWindow *window = self.appWindow;
229 | [window addSubview:self];
230 | [window bringSubviewToFront:self];
231 | }
232 |
233 | [self addSubview:self.maskPanel];
234 | [self addSubview:self.contentView];
235 | [self bringSubviewToFront:self.contentView];
236 |
237 | [self.contentView addSubview:self.indicator];
238 | [self.contentView addSubview:self.textLabel];
239 | }
240 |
241 | /// Prepares to begin animating.
242 | - (void)beginAnimating
243 | {
244 | [self.indicator startAnimating];
245 |
246 | self.alpha = 0.f;
247 | [UIView animateWithDuration:0.3 animations:^{
248 | self.alpha = 1.f;
249 | }];
250 | }
251 |
252 | /// Finds out the current view controller.
253 | - (UIViewController *)appCurrentViewController
254 | {
255 | UIViewController *vc = self.appWindow.rootViewController;
256 | while (1) {
257 | if (vc.presentedViewController) {
258 | vc = vc.presentedViewController;
259 | } else if ([vc isKindOfClass:UITabBarController.class]) {
260 | vc = ((UITabBarController *)vc).selectedViewController;
261 | } else if ([vc isKindOfClass:UINavigationController.class]) {
262 | vc = ((UINavigationController *)vc).visibleViewController;
263 | } else {
264 | if (vc.childViewControllers.count > 0) {
265 | vc = vc.childViewControllers.lastObject;
266 | }
267 | break;
268 | }
269 | }
270 | return vc;
271 | }
272 |
273 | - (void)layoutSubviews
274 | {
275 | CGFloat self_w = 0.f;
276 | CGFloat self_h = 0.f;
277 |
278 | UIView *view = self.superview;
279 | if (view) {
280 | self_w = view.bounds.size.width;
281 | self_h = view.bounds.size.height;
282 | } else {
283 | self_w = SCREEN_W;
284 | self_h = SCREEN_H;
285 | }
286 | self.frame = CGRectMake(0, 0, self_w, self_h);
287 | self.maskPanel.frame = CGRectMake(0, 0, self_w, self_h);
288 | self.contentView.center = CGPointMake(self_w/2, self_h/2);
289 | }
290 |
291 | - (void)dealloc
292 | {
293 | #if DEBUG
294 | NSLog(@"%s", __FUNCTION__);
295 | #endif
296 | }
297 |
298 | @end
299 |
--------------------------------------------------------------------------------
/unity-iap/objc/External/NSObject+DYFAdd.h:
--------------------------------------------------------------------------------
1 | //
2 | // NSObject+DYFAdd.h
3 | //
4 | // Created by Tenfay on 2014/11/4. ( https://github.com/itenfay/DYFStoreKit )
5 | // Copyright © 2014 Tenfay. All rights reserved.
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in
15 | // all copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | // THE SOFTWARE.
24 | //
25 |
26 | #import
27 |
28 | @interface NSObject (DYFAdd)
29 |
30 | /** The app's key window.
31 | */
32 | - (UIWindow *)mainWindow;
33 |
34 | /** The view controller associated with the currently visible view.
35 | */
36 | - (UIViewController *)currentViewController;
37 |
38 | /** Shows the tips for user.
39 | */
40 | - (void)showTipsMessage:(NSString *)message;
41 |
42 | /** Shows an alert view controller.
43 | */
44 | - (void)showAlertWithTitle:(NSString *)title
45 | message:(NSString *)message
46 | cancelButtonTitle:(NSString *)cancelButtonTitle
47 | cancel:(void (^)(UIAlertAction *action))cancelHandler
48 | confirmButtonTitle:(NSString *)confirmButtonTitle
49 | execute:(void (^)(UIAlertAction *action))executableHandler;
50 |
51 | /** Shows a loading panel.
52 | */
53 | - (void)showLoading:(NSString *)text;
54 |
55 | /** Hides a loading panel.
56 | */
57 | - (void)hideLoading;
58 |
59 | @end
60 |
--------------------------------------------------------------------------------
/unity-iap/objc/External/NSObject+DYFAdd.m:
--------------------------------------------------------------------------------
1 | //
2 | // NSObject+DYFAdd.m
3 | //
4 | // Created by Tenfay on 2014/11/4. ( https://github.com/itenfay/DYFStoreKit )
5 | // Copyright © 2014 Tenfay. All rights reserved.
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in
15 | // all copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | // THE SOFTWARE.
24 | //
25 |
26 | #import "NSObject+DYFAdd.h"
27 | #import "DYFLoadingView.h"
28 | #import
29 |
30 | NSString *const LoadingViewKey = @"LoadingViewKey";
31 |
32 | @implementation NSObject (DYFAdd)
33 |
34 | - (UIWindow *)mainWindow
35 | {
36 | UIWindow *window;
37 | NSMutableArray *windowArray = [NSMutableArray arrayWithCapacity:0];
38 | UIApplication *sharedApp = UIApplication.sharedApplication;
39 | if (@available(iOS 13.0, *)) {
40 | NSMutableArray *sceneArray = [NSMutableArray arrayWithCapacity:0];
41 | for (UIScene *scene in sharedApp.connectedScenes) {
42 | if (scene.activationState == UISceneActivationStateForegroundActive &&
43 | [scene isKindOfClass:UIWindowScene.class]) {
44 | [sceneArray addObject:(UIWindowScene *)scene];
45 | }
46 | }
47 | UIWindowScene *scene = sceneArray.firstObject;
48 | for (UIWindow *w in scene.windows) {
49 | if (w.isKeyWindow) { [windowArray addObject:w]; }
50 | }
51 | } else {
52 | for (UIWindow *w in sharedApp.windows) {
53 | if (w.isKeyWindow) { [windowArray addObject:w]; }
54 | }
55 | }
56 | window = windowArray.firstObject;
57 | return window;
58 | }
59 |
60 | - (UIViewController *)currentViewController
61 | {
62 | UIWindow *window = [self mainWindow];
63 | return [self findCurrentViewControllerFrom:window.rootViewController];
64 | }
65 |
66 | - (UIViewController *)findCurrentViewControllerFrom:(UIViewController *)viewController
67 | {
68 | UIViewController *vc = viewController;
69 | while (1) {
70 | if (vc.presentedViewController) {
71 | vc = vc.presentedViewController;
72 | } else if ([vc isKindOfClass:UITabBarController.class]) {
73 | vc = ((UITabBarController *)vc).selectedViewController;
74 | } else if ([vc isKindOfClass:UINavigationController.class]) {
75 | vc = ((UINavigationController *)vc).visibleViewController;
76 | } else {
77 | if (vc.childViewControllers.count > 0) {
78 | vc = vc.childViewControllers.lastObject;
79 | }
80 | break;
81 | }
82 | }
83 | return vc;
84 | }
85 |
86 | - (void)showTipsMessage:(NSString *)message
87 | {
88 | if ([self.currentViewController isKindOfClass:UIAlertController.class]) {
89 | return;
90 | }
91 |
92 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:@"" preferredStyle:UIAlertControllerStyleAlert];
93 |
94 | [self.currentViewController presentViewController:alertController animated:YES completion:NULL];
95 |
96 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)),
97 | dispatch_get_main_queue(), ^{
98 | [alertController dismissViewControllerAnimated:YES completion:NULL];
99 | });
100 | }
101 |
102 | - (void)showAlertWithTitle:(NSString *)title
103 | message:(NSString *)message
104 | cancelButtonTitle:(NSString *)cancelButtonTitle
105 | cancel:(void (^)(UIAlertAction *))cancelHandler
106 | confirmButtonTitle:(NSString *)confirmButtonTitle
107 | execute:(void (^)(UIAlertAction *))executableHandler
108 | {
109 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
110 |
111 | if (cancelButtonTitle && cancelButtonTitle.length > 0) {
112 | UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:cancelButtonTitle style:UIAlertActionStyleCancel handler:cancelHandler];
113 | [alertController addAction:cancelAction];
114 | }
115 |
116 | if (confirmButtonTitle && confirmButtonTitle.length > 0) {
117 | UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:confirmButtonTitle style:UIAlertActionStyleDefault handler:executableHandler];
118 | [alertController addAction:confirmAction];
119 | }
120 |
121 | [self.currentViewController presentViewController:alertController animated:YES completion:NULL];
122 | }
123 |
124 | - (void)showLoading:(NSString *)text
125 | {
126 | id value = objc_getAssociatedObject(self, &LoadingViewKey);
127 | if (value) {
128 | return;
129 | }
130 | DYFLoadingView *loadingView = [[DYFLoadingView alloc] init];
131 | loadingView.show(text);
132 | loadingView.color = COLOR_RGBA(10, 10, 10, 0.75);
133 | loadingView.indicatorColor = COLOR_RGB(54, 205, 64);
134 | loadingView.textColor = COLOR_RGB(248, 248, 248);
135 | objc_setAssociatedObject(self, &LoadingViewKey, loadingView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
136 | }
137 |
138 | - (void)hideLoading
139 | {
140 | id value = objc_getAssociatedObject(self, &LoadingViewKey);
141 | if (!value) {
142 | return;
143 | }
144 | DYFLoadingView *loadingView = (DYFLoadingView *)value;
145 | [loadingView hide];
146 | objc_setAssociatedObject(self, &LoadingViewKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
147 | }
148 |
149 | @end
150 |
--------------------------------------------------------------------------------
/unity-iap/objc/External/UIView+DYFAdd.h:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+DYFAdd.h
3 | //
4 | // Created by Tenfay on 2014/11/4. ( https://github.com/itenfay/DYFStoreKit )
5 | // Copyright © 2014 Tenfay. All rights reserved.
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in
15 | // all copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | // THE SOFTWARE.
24 | //
25 |
26 | #import
27 |
28 | /** The block is called when the corners are set.
29 |
30 | @param rectCorner The corners of a rectangle.
31 | @param radius The radius of each corner.
32 | */
33 | typedef void (^DYFSetCornerBlock)(UIRectCorner rectCorner, CGFloat radius);
34 |
35 | /** The block is called when the border is set.
36 |
37 | @param rectCorner The corners of a rectangle.
38 | @param radius The radius of each corner.
39 | @param lineWidth Specifies the line width of the shape’s path.
40 | @param color The color used to stroke the shape’s path.
41 | */
42 | typedef void (^DYFSetBorderBlock)(UIRectCorner rectCorner, CGFloat radius, CGFloat lineWidth, UIColor *color);
43 |
44 | @interface UIView (DYFAdd)
45 |
46 | /** This method is used to set the corner.
47 |
48 | @return A block with a `UIRectCorner` value and radius.
49 | */
50 | - (DYFSetCornerBlock)setCorner;
51 |
52 | /** This method is used to set the border.
53 |
54 | @return A block with a `UIRectCorner` value, radius, line width and a color.
55 | */
56 | - (DYFSetBorderBlock)setBorder;
57 |
58 | @end
59 |
--------------------------------------------------------------------------------
/unity-iap/objc/External/UIView+DYFAdd.m:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+DYFAdd.m
3 | //
4 | // Created by Tenfay on 2014/11/4. ( https://github.com/itenfay/DYFStoreKit )
5 | // Copyright © 2014 Tenfay. All rights reserved.
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in
15 | // all copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | // THE SOFTWARE.
24 | //
25 |
26 | #import "UIView+DYFAdd.h"
27 |
28 | @implementation UIView (DYFAdd)
29 |
30 | - (DYFSetCornerBlock)setCorner
31 | {
32 | DYFSetCornerBlock block = ^(UIRectCorner rectCorner, CGFloat radius) {
33 | CAShapeLayer *maskLayer = [CAShapeLayer layer];
34 | CGFloat w = self.bounds.size.width;
35 | CGFloat h = self.bounds.size.height;
36 | maskLayer.frame = CGRectMake(0, 0, w, h);
37 |
38 | UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:rectCorner cornerRadii:CGSizeMake(radius, radius)];
39 | maskLayer.path = path.CGPath;
40 |
41 | [self.layer setMask:maskLayer];
42 | };
43 | return block;
44 | }
45 |
46 | - (DYFSetBorderBlock)setBorder
47 | {
48 | DYFSetBorderBlock block = ^(UIRectCorner rectCorner, CGFloat radius, CGFloat lineWidth, UIColor *color) {
49 | CAShapeLayer *maskLayer = [CAShapeLayer layer];
50 | CGFloat w = self.bounds.size.width;
51 | CGFloat h = self.bounds.size.height;
52 | maskLayer.frame = CGRectMake(0, 0, w, h);
53 |
54 | CAShapeLayer *borderLayer = [CAShapeLayer layer];
55 | borderLayer.frame = CGRectMake(0, 0, w, h);
56 | borderLayer.lineWidth = lineWidth;
57 | borderLayer.strokeColor = color.CGColor;
58 | borderLayer.fillColor = [UIColor clearColor].CGColor;
59 |
60 | UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:rectCorner cornerRadii:CGSizeMake(radius, radius)];
61 | borderLayer.path = path.CGPath;
62 | maskLayer.path = path.CGPath;
63 |
64 | [self.layer insertSublayer:borderLayer atIndex:0];
65 | [self.layer setMask:maskLayer];
66 | };
67 | return block;
68 | }
69 |
70 | @end
71 |
--------------------------------------------------------------------------------
/unity-iap/objc/StoreManager/DYFStoreManager.h:
--------------------------------------------------------------------------------
1 | //
2 | // DYFStoreManager.h
3 | //
4 | // Created by Tenfay on 2014/11/4. ( https://github.com/itenfay/Unity-iOS-InAppPurchase )
5 | // Copyright © 2014 Tenfay. All rights reserved.
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in
15 | // all copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | // THE SOFTWARE.
24 | //
25 |
26 | #import
27 |
28 | @interface DYFStoreManager : NSObject
29 |
30 | /** Constructs a store manager singleton with class method.
31 | */
32 | + (instancetype)shared;
33 |
34 | /** Adds a store observer.
35 | */
36 | - (void)addStoreObserver;
37 |
38 | /** Removes a store observer.
39 | */
40 | - (void)removeStoreObserver;
41 |
42 | /** Requests payment of the product with the given product identifier.
43 | */
44 | - (void)addPayment:(NSString *)productIdentifier;
45 |
46 | /** Requests payment of the product with the given product identifier, an opaque identifier for the user’s account on your system.
47 | */
48 | - (void)addPayment:(NSString *)productIdentifier userIdentifier:(NSString *)userIdentifier;
49 |
50 | /** Requests to restore previously completed transactions.
51 | */
52 | - (void)restoreTransactions;
53 |
54 | /** Requests to restore previously completed transactions with an opaque identifier for the user’s account on your system.
55 | */
56 | - (void)restoreTransactions:(NSString *)userIdentifier;
57 |
58 | /** Requests to refresh the App Store receipt in case the receipt is invalid or missing.
59 | */
60 | - (void)refreshReceipt;
61 |
62 | /** Queries those incompleted transactions from keychain.
63 | */
64 | - (void)queryIncompletedTransactions;
65 |
66 | @end
67 |
--------------------------------------------------------------------------------
/unity-iap/objc/StoreManager/DYFStoreManager.mm:
--------------------------------------------------------------------------------
1 | //
2 | // DYFStoreManager.m
3 | //
4 | // Created by Tenfay on 2014/11/4. ( https://github.com/itenfay/Unity-iOS-InAppPurchase )
5 | // Copyright © 2014 Tenfay. All rights reserved.
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in
15 | // all copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | // THE SOFTWARE.
24 | //
25 |
26 | #import "DYFStoreManager.h"
27 | #import "UnityIAPConnector.h"
28 | #import "DYFStore.h"
29 | #import "DYFStoreUserDefaultsPersistence.h"
30 | //#import "NSObject+DYFAdd.h" //Optional
31 | //#import "DYFStoreReceiptVerifier.h" //Optional
32 |
33 | //@interface DYFStoreManager () //Optional
34 | @interface DYFStoreManager ()
35 |
36 | @property (nonatomic, strong) DYFStoreNotificationInfo *purchaseInfo;
37 | @property (nonatomic, strong) DYFStoreNotificationInfo *downloadInfo;
38 |
39 | //@property (nonatomic, strong) DYFStoreReceiptVerifier *receiptVerifier;
40 |
41 | @end
42 |
43 | @implementation DYFStoreManager
44 |
45 | // Provides a global static variable.
46 | static DYFStoreManager *_instance = nil;
47 |
48 | + (instancetype)shared
49 | {
50 | static dispatch_once_t onceToken;
51 | dispatch_once(&onceToken, ^{
52 | _instance = [[self alloc] init];
53 | });
54 | return _instance;
55 | }
56 |
57 | - (instancetype)init
58 | {
59 | self = [super init];
60 | if (self) {
61 | [self setup];
62 | }
63 | return self;
64 | }
65 |
66 | - (void)setup
67 | {
68 |
69 | }
70 |
71 | - (void)addStoreObserver
72 | {
73 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(processPurchaseNotification:) name:DYFStorePurchasedNotification object:nil];
74 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(processDownloadNotification:) name:DYFStoreDownloadedNotification object:nil];
75 | }
76 |
77 | - (void)removeStoreObserver
78 | {
79 | [NSNotificationCenter.defaultCenter removeObserver:self
80 | name:DYFStorePurchasedNotification
81 | object:nil];
82 | [NSNotificationCenter.defaultCenter removeObserver:self
83 | name:DYFStoreDownloadedNotification
84 | object:nil];
85 | }
86 |
87 | - (void)addPayment:(NSString *)productIdentifier
88 | {
89 | [self addPayment:productIdentifier userIdentifier:nil];
90 | }
91 |
92 | - (void)addPayment:(NSString *)productIdentifier userIdentifier:(NSString *)userIdentifier
93 | {
94 | // Initiate purchase request.
95 | //[self showLoading:@"Waiting..."];
96 | [DYFStore.defaultStore purchaseProduct:productIdentifier userIdentifier:userIdentifier];
97 | }
98 |
99 | - (void)restoreTransactions
100 | {
101 | [self restoreTransactions:nil];
102 | }
103 |
104 | - (void)restoreTransactions:(NSString *)userIdentifier
105 | {
106 | DYFStoreLog(@"userIdentifier: %@", userIdentifier);
107 | //[self showLoading:@"Restoring..."];
108 | [DYFStore.defaultStore restoreTransactions:userIdentifier];
109 | }
110 |
111 | - (void)processDeferredPurchase
112 | {
113 | MKVContainer *item = [MKVContainer container];
114 | [item setValue:@"The purchase has been deferred" forKey:@"m_desc"];
115 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_PURCHASE_DEFERRED, item);
116 | }
117 |
118 | - (void)processPurchaseInProgress
119 | {
120 | MKVContainer *item = [MKVContainer container];
121 | [item setValue:@"The purchase is in progress" forKey:@"m_desc"];
122 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_PURCHASE_IN_PROGRESS, item);
123 | }
124 |
125 | - (void)processCancelledPurchase
126 | {
127 | MKVContainer *item = [MKVContainer container];
128 | [item setValue:@"The purchase has been cancelled by user" forKey:@"m_desc"];
129 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_PURCHASE_CANCELLED, item);
130 | }
131 |
132 | - (void)processFailedPurchase
133 | {
134 | int msgCode = 0;
135 | if (self.purchaseInfo.state == DYFStorePurchaseStateFailed) {
136 | msgCode = UN_MSG_CBTYPE_PURCHASE_FAILED;
137 | } else {
138 | msgCode = UN_MSG_CBTYPE_FAIL_TO_RESTORE_PURCHASE;
139 | }
140 |
141 | NSInteger code = self.purchaseInfo.error.code;
142 | NSString *value = self.purchaseInfo.error.userInfo[NSLocalizedDescriptionKey];
143 | NSString *msg = value ?: self.purchaseInfo.error.localizedDescription;
144 |
145 | MKVContainer *item = [MKVContainer container];
146 | [item setValue:@(code) forKey:@"err_code"];
147 | [item setValue:msg forKey:@"err_desc"];
148 | UNCallbackMessageDataToUnity(msgCode, item);
149 | }
150 |
151 | - (void)processPurchaseNotification:(NSNotification *)notification
152 | {
153 | //[self hideLoading];
154 | self.purchaseInfo = notification.object;
155 | switch (self.purchaseInfo.state) {
156 | case DYFStorePurchaseStatePurchasing:
157 | //[self showLoading:@"Purchasing..."];
158 | [self processPurchaseInProgress];
159 | break;
160 | case DYFStorePurchaseStateCancelled:
161 | // The user cancel the purchase.
162 | //[self sendNotice:@"You cancel the purchase"];
163 | [self processCancelledPurchase];
164 | break;
165 | case DYFStorePurchaseStateFailed:
166 | //[self sendNotice:[NSString stringWithFormat:@"An error occurred, code is %zi.", self.purchaseInfo.error.code]];
167 | [self processFailedPurchase];
168 | break;
169 | case DYFStorePurchaseStateSucceeded:
170 | case DYFStorePurchaseStateRestored:
171 | [self completePayment];
172 | break;
173 | case DYFStorePurchaseStateRestoreFailed:
174 | //[self sendNotice:[NSString stringWithFormat:@"An error occurred, code is %zi.", self.purchaseInfo.error.code]];
175 | [self processFailedPurchase];
176 | break;
177 | case DYFStorePurchaseStateDeferred:
178 | // Deferred
179 | [self processDeferredPurchase];
180 | break;
181 | default:
182 | break;
183 | }
184 | }
185 |
186 | - (void)processDownloadNotification:(NSNotification *)notification
187 | {
188 | self.downloadInfo = notification.object;
189 | switch (self.downloadInfo.downloadState) {
190 | case DYFStoreDownloadStateStarted:
191 | DYFStoreLog(@"The download started");
192 | break;
193 | case DYFStoreDownloadStateInProgress:
194 | DYFStoreLog(@"The download progress: %.2f%%", self.downloadInfo.downloadProgress);
195 | break;
196 | case DYFStoreDownloadStateCancelled:
197 | DYFStoreLog(@"The download cancelled");
198 | break;
199 | case DYFStoreDownloadStateFailed:
200 | DYFStoreLog(@"The download failed");
201 | break;
202 | case DYFStoreDownloadStateSucceeded:
203 | DYFStoreLog(@"The download succeeded: 100%%");
204 | break;
205 | default:
206 | break;
207 | }
208 | }
209 |
210 | - (void)completePayment
211 | {
212 | DYFStoreNotificationInfo *info = self.purchaseInfo;
213 | DYFStoreUserDefaultsPersistence *persister = [[DYFStoreUserDefaultsPersistence alloc] init];
214 |
215 | NSString *identifier = info.transactionIdentifier;
216 | if (![persister containsTransaction:identifier]) {
217 | [self storeReceipt];
218 | return;
219 | }
220 |
221 | DYFStoreTransaction *transaction = [persister retrieveTransaction:identifier];
222 | [self verifyReceipt:transaction];
223 | }
224 |
225 | - (void)storeReceipt
226 | {
227 | DYFStoreLog();
228 | NSURL *receiptURL = DYFStore.receiptURL;
229 | NSData *data = [NSData dataWithContentsOfURL:receiptURL];
230 | if (!data || data.length == 0) {
231 | [self sendNoticeToRefreshReceipt];
232 | return;
233 | }
234 |
235 | DYFStoreNotificationInfo *info = self.purchaseInfo;
236 | DYFStoreUserDefaultsPersistence *persister = [[DYFStoreUserDefaultsPersistence alloc] init];
237 |
238 | DYFStoreTransaction *transaction = [[DYFStoreTransaction alloc] init];
239 | if (info.state == DYFStorePurchaseStateSucceeded) {
240 | transaction.state = DYFStoreTransactionStatePurchased;
241 | } else if (info.state == DYFStorePurchaseStateRestored) {
242 | transaction.state = DYFStoreTransactionStateRestored;
243 | }
244 |
245 | transaction.productIdentifier = info.productIdentifier;
246 | transaction.userIdentifier = info.userIdentifier;
247 | transaction.transactionIdentifier = info.transactionIdentifier;
248 | transaction.transactionTimestamp = info.transactionDate.timestamp;
249 | transaction.originalTransactionTimestamp = info.originalTransactionDate.timestamp;
250 | transaction.originalTransactionIdentifier = info.originalTransactionIdentifier;
251 |
252 | transaction.transactionReceipt = data.base64EncodedString;
253 | [persister storeTransaction:transaction];
254 |
255 | [self verifyReceipt:transaction];
256 | }
257 |
258 | - (void)sendNoticeToRefreshReceipt
259 | {
260 | MKVContainer *item = [MKVContainer container];
261 | [item setValue:@"The receipt needs to be refreshed" forKey:@"m_desc"];
262 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_REFRESH_RECEIPT, item);
263 | }
264 |
265 | - (void)refreshReceipt
266 | {
267 | DYFStoreLog();
268 | //[self showLoading:@"Refresh receipt..."];
269 | [DYFStore.defaultStore refreshReceiptOnSuccess:^{
270 | [self storeReceipt];
271 | } failure:^(NSError *error) {
272 | [self failToRefreshReceipt:error];
273 | }];
274 | }
275 |
276 | - (void)failToRefreshReceipt:(NSError *)error
277 | {
278 | DYFStoreLog();
279 | //[self hideLoading];
280 | MKVContainer *item = [MKVContainer container];
281 | [item setValue:@(error.code) forKey:@"err_code"];
282 | [item setValue:error.localizedDescription forKey:@"err_desc"];
283 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_FAIL_TO_REFRESH_RECEIPT, item);
284 | }
285 |
286 | - (void)verifyReceipt:(DYFStoreTransaction *)transaction
287 | {
288 | NSUInteger state = transaction.state;
289 | NSString *productId = transaction.productIdentifier;
290 | NSString *userId = transaction.userIdentifier;
291 | NSString *transId = transaction.transactionIdentifier;
292 | NSString *transTs = transaction.transactionTimestamp;
293 | NSString *orgTransId = transaction.originalTransactionIdentifier;
294 | NSString *orgTransTs = transaction.originalTransactionTimestamp;
295 | NSString *receipt = transaction.transactionReceipt;
296 |
297 | DYFStoreLog(@"transaction.state: %zi", state);
298 | DYFStoreLog(@"transaction.productIdentifier: %@", productId);
299 | DYFStoreLog(@"transaction.userIdentifier: %@", userId);
300 | DYFStoreLog(@"transaction.transactionIdentifier: %@", transId);
301 | DYFStoreLog(@"transaction.transactionTimestamp: %@", transTs);
302 | DYFStoreLog(@"transaction.originalTransactionIdentifier: %@", orgTransId);
303 | DYFStoreLog(@"transaction.originalTransactionTimestamp: %@", orgTransTs);
304 | DYFStoreLog(@"transaction.transactionReceipt: %@", receipt);
305 |
306 | int msgCode = 0;
307 | if (state == (NSUInteger)DYFStoreTransactionStatePurchased) {
308 | msgCode = UN_MSG_CBTYPE_PURCHASE_SUCCEEDED;
309 | } else {
310 | msgCode = UN_MSG_CBTYPE_PURCHASE_RESTORED;
311 | }
312 |
313 | MKVContainer *item = [MKVContainer container];
314 | [item setValue:@(state) forKey:@"t_state"];
315 | [item setValue:productId forKey:@"p_id"];
316 | [item setValue:userId ?: [NSNull null] forKey:@"u_id"];
317 | [item setValue:transId forKey:@"t_id"];
318 | [item setValue:transTs forKey:@"t_ts"];
319 | [item setValue:orgTransId ?: [NSNull null] forKey:@"orgt_id"];
320 | [item setValue:orgTransTs ?: [NSNull null] forKey:@"orgt_ts"];
321 | [item setValue:receipt forKey:@"t_receipt"];
322 | UNCallbackMessageDataToUnity(msgCode, item);
323 | }
324 |
325 | - (void)queryIncompletedTransactions
326 | {
327 | DYFStoreUserDefaultsPersistence *persister = [[DYFStoreUserDefaultsPersistence alloc] init];
328 | NSArray *arr = [persister retrieveTransactions];
329 | if (arr && arr.count > 0) {
330 | NSMutableArray *mArr = [NSMutableArray array];
331 | for (DYFStoreTransaction *t in arr) {
332 | MKVContainer *item = [MKVContainer container];
333 | [item setValue:@(t.state) forKey:@"t_state"];
334 | [item setValue:t.productIdentifier forKey:@"p_id"];
335 | [item setValue:t.userIdentifier ?: [NSNull null] forKey:@"u_id"];
336 | [item setValue:t.transactionIdentifier forKey:@"t_id"];
337 | [item setValue:t.transactionTimestamp forKey:@"t_ts"];
338 | [item setValue:t.originalTransactionIdentifier ?: [NSNull null] forKey:@"orgt_id"];
339 | [item setValue:t.originalTransactionTimestamp ?: [NSNull null] forKey:@"orgt_ts"];
340 | [item setValue:t.transactionReceipt forKey:@"t_receipt"];
341 | [mArr addObject:item];
342 | }
343 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_INCOMPLETED_TRANSACTIONS, mArr);
344 | return;
345 | }
346 |
347 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_INCOMPLETED_TRANSACTIONS, OCEmptyString);
348 | }
349 |
350 | //- (DYFStoreReceiptVerifier *)receiptVerifier
351 | //{
352 | // if (!_receiptVerifier) {
353 | // _receiptVerifier = [[DYFStoreReceiptVerifier alloc] init];
354 | // _receiptVerifier.delegate = self;
355 | // }
356 | // return _receiptVerifier;
357 | //}
358 |
359 | // It is better to use your own server to obtain the parameters uploaded from the client to verify the receipt from the app store server (C -> Uploaded Parameters -> S -> App Store S -> S -> Receive And Parse Data -> C).
360 | // If the receipts are verified by your own server, the client needs to upload these parameters, such as: "transaction identifier, bundle identifier, product identifier, user identifier, shared sceret(Subscription), receipt(Safe URL Base64), original transaction identifier(Optional), original transaction time(Optional) and the device information, etc.".
361 | //- (void)verifyReceiptByClient:(NSData *)receiptData
362 | //{
363 | // DYFStoreLog();
364 | // [self hideLoading];
365 | // [self showLoading:@"Verify receipt..."];
366 | //
367 | // NSData *data = receiptData ?: [NSData dataWithContentsOfURL:DYFStore.receiptURL];
368 | // DYFStoreLog(@"data: %@", data);
369 | //
370 | // [self.receiptVerifier verifyReceipt:data];
371 | // // Only used for receipts that contain auto-renewable subscriptions.
372 | // //[_receiptVerifier verifyReceipt:data sharedSecret:@"A43512564ACBEF687924646CAFEFBDCAEDF4155125657"];
373 | //}
374 |
375 | //- (void)retryToVerifyReceipt
376 | //{
377 | // DYFStoreNotificationInfo *info = self.purchaseInfo;
378 | // DYFStoreUserDefaultsPersistence *persister = [[DYFStoreUserDefaultsPersistence alloc] init];
379 | //
380 | // NSString *identifier = info.transactionIdentifier;
381 | // DYFStoreTransaction *transaction = [persister retrieveTransaction:identifier];
382 | // NSData *receiptData = transaction.transactionReceipt.base64DecodedData;
383 | // [self verifyReceiptByClient:receiptData];
384 | //}
385 |
386 | //- (void)verifyReceiptDidFinish:(nonnull DYFStoreReceiptVerifier *)verifier didReceiveData:(nullable NSDictionary *)data {
387 | // DYFStoreLog(@"data: %@", data);
388 | // [self hideLoading];
389 | // [self showTipsMessage:@"Purchase Successfully"];
390 | // // Tips: Purchase Successfully!
391 | //}
392 |
393 | //- (void)verifyReceipt:(nonnull DYFStoreReceiptVerifier *)verifier didFailWithError:(nonnull NSError *)error {
394 | // // Prints the reason of the error.
395 | // DYFStoreLog(@"error: %zi, %@", error.code, error.localizedDescription);
396 | // [self hideLoading];
397 | //
398 | // // An error occurs that has nothing to do with in-app purchase. Maybe it's the internet.
399 | // if (error.code < 21000) {
400 | // // After several attempts, you can cancel refreshing receipt.
401 | // //Fail to verify receipt!
402 | // //[self retryToVerifyReceipt];
403 | // return;
404 | // }
405 | //
406 | // // Tips: Fail to purchase the product!
407 | //}
408 |
409 | //- (void)sendNotice:(NSString *)message
410 | //{
411 | // [self showAlertWithTitle:NSLocalizedStringFromTable(@"Notification", nil, @"")
412 | // message:message
413 | // cancelButtonTitle:nil
414 | // cancel:NULL
415 | // confirmButtonTitle:NSLocalizedStringFromTable(@"I see", nil, @"")
416 | // execute:^(UIAlertAction *action) {
417 | // DYFStoreLog(@"Alert action title: %@", action.title);
418 | // }];
419 | //}
420 |
421 | - (void)dealloc
422 | {
423 |
424 | }
425 |
426 | @end
427 |
--------------------------------------------------------------------------------
/unity-iap/objc/UnityIAPConnector.h:
--------------------------------------------------------------------------------
1 | //
2 | // UnityIAPConnector
3 | //
4 | // Created by Tenfay on 2020/4/16. ( https://github.com/itenfay/Unity-iOS-InAppPurchase )
5 | // Copyright © 2020 Tenfay. All rights reserved.
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in
15 | // all copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | // THE SOFTWARE.
24 | //
25 |
26 | #import
27 |
28 | /// Note: This function is declared in UnityInterface.h.
29 | //extern void UnitySendMessage(const char* obj, const char* method, const char* msg);
30 |
31 | /// Type define for mutable dictionary.
32 | typedef NSMutableDictionary MKVContainer;
33 |
34 | typedef NS_ENUM(int, UNMsgCallbackType)
35 | {
36 | // The device is not able or allowed to make payments.
37 | UN_MSG_CBTYPE_CANNOT_MAKE_PAYMENTS = 1,
38 | // The product has been got successfully.
39 | UN_MSG_CBTYPE_GET_PRODUCT_SUCCESSFULLY = 2,
40 | // The product has failed to be got.
41 | UN_MSG_CBTYPE_FAIL_TO_GET_PRODUCT = 3,
42 | // There is no product for sale.
43 | UN_MSG_CBTYPE_NO_PRODUCT_FOR_SALE = 4,
44 | // A set of products have been got successfully.
45 | UN_MSG_CBTYPE_GET_PRODUCTS_SUCCESSFULLY = 5,
46 | // A set of products have failed to be got.
47 | UN_MSG_CBTYPE_FAIL_TO_GET_PRODUCTS = 6,
48 | // There is no products for sale.
49 | UN_MSG_CBTYPE_NO_PRODUCTS_FOR_SALE = 7,
50 | // The purchase has been deferred.
51 | UN_MSG_CBTYPE_PURCHASE_DEFERRED = 8,
52 | // The purchase is in progress.
53 | UN_MSG_CBTYPE_PURCHASE_IN_PROGRESS = 9,
54 | // The purchase has been cancelled by user.
55 | UN_MSG_CBTYPE_PURCHASE_CANCELLED = 10,
56 | // The purchase has failed.
57 | UN_MSG_CBTYPE_PURCHASE_FAILED = 11,
58 | // The purchase is successful.
59 | UN_MSG_CBTYPE_PURCHASE_SUCCEEDED = 12,
60 | // The purchase has failed to be restored.
61 | UN_MSG_CBTYPE_FAIL_TO_RESTORE_PURCHASE = 13,
62 | // The purchase has been restored successfully.
63 | UN_MSG_CBTYPE_PURCHASE_RESTORED = 14,
64 | // The receipt needs to be refreshed.
65 | UN_MSG_CBTYPE_REFRESH_RECEIPT = 15,
66 | // The receipt has failed to be refreshed.
67 | UN_MSG_CBTYPE_FAIL_TO_REFRESH_RECEIPT = 16,
68 | // The incompleted transactions were queried, then continue to verify receipt.
69 | UN_MSG_CBTYPE_INCOMPLETED_TRANSACTIONS = 17
70 | };
71 |
72 | /// The empty string for Objective-C.
73 | extern NSString *const OCEmptyString;
74 |
75 | extern void UNCallbackMessageDataToUnity(int msgCode, id msgData);
76 |
77 | @interface UnityIAPConnector : NSObject
78 |
79 | + (NSString *)jsonWithObject:(id)object;
80 | + (id)objectWithJson:(NSString *)json;
81 |
82 | @end
83 |
84 | @interface NSMutableDictionary (UNTypeDef)
85 |
86 | + (instancetype)container;
87 |
88 | @end
89 |
--------------------------------------------------------------------------------
/unity-iap/objc/UnityIAPConnector.mm:
--------------------------------------------------------------------------------
1 | //
2 | // UnityIAPConnector.mm
3 | //
4 | // Created by Tenfay on 2020/4/16. ( https://github.com/itenfay/Unity-iOS-InAppPurchase )
5 | // Copyright © 2020 Tenfay. All rights reserved.
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in
15 | // all copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | // THE SOFTWARE.
24 | //
25 |
26 | #import "DYFStoreManager.h"
27 | #import "UnityIAPConnector.h"
28 | #import "DYFStore.h"
29 | #import "DYFStoreUserDefaultsPersistence.h"
30 | #import "UnityInterface.h"
31 |
32 | /// This is used to store the name of the `GameObject` object.
33 | static NSString *s_callbackGameObject;
34 | /// This is used to store a function name of the `GameObject` object.
35 | static NSString *s_callbackFunc;
36 |
37 | /// This function (UnitySendMessage) is declared in UnityInterface.h.
38 | //void UnitySendMessage(const char* obj, const char* method, const char* msg) {} //For testing
39 |
40 | #if !defined(__UN_SEND_MSG)
41 | #define __UN_SEND_MSG(s_msg) UnitySendMessage([s_callbackGameObject UTF8String], [s_callbackFunc UTF8String], [s_msg UTF8String])
42 | #endif
43 |
44 | /// A string created by copying the data from bytes.
45 | #if !defined(__OBJC_STRING)
46 | #define __OBJC_STRING(c_str) ({ NSString *s = nil; if (c_str) { s = [NSString stringWithUTF8String:c_str]; } s; })
47 | #endif
48 |
49 | /// The empty string for Objective-C.
50 | NSString *const OCEmptyString = @"";
51 |
52 | /// This function is used to callback message data to unity.
53 | void UNCallbackMessageDataToUnity(int msgCode, id msgData)
54 | {
55 | MKVContainer *dataBody = [MKVContainer container];
56 | [dataBody setValue:@(msgCode) forKey:@"msg_code"];
57 | [dataBody setValue:msgData forKey:@"msg_data"];
58 |
59 | NSString *msg = [UnityIAPConnector jsonWithObject:dataBody];
60 | msg ? __UN_SEND_MSG(msg) : __UN_SEND_MSG(OCEmptyString);
61 | }
62 |
63 | /// Returns the localized price of a given product.
64 | static NSString *UNLocalizedPrice(SKProduct *product)
65 | {
66 | return [DYFStore.defaultStore localizedPriceOfProduct:product];
67 | }
68 |
69 | #if defined(_cplusplus)
70 | extern "C"{
71 | #endif
72 |
73 | /// Initializes message callback for Unity.
74 | /// @param callbackGameObject The gameObject name comes from Unity.
75 | /// @param callbackFunc The function name comes from Unity.
76 | void DYFInitUnityMsgCallback(const char* callbackGameObject, const char* callbackFunc)
77 | {
78 | NSCAssert(callbackGameObject != NULL, @"The callback gameobject is null");
79 | NSCAssert(callbackFunc != NULL, @"The callback function is null");
80 | s_callbackGameObject = __OBJC_STRING(callbackGameObject);
81 | s_callbackFunc = __OBJC_STRING(callbackFunc);
82 | #if DEBUG
83 | NSLog(@"[::] callbackGameObject: %@, callbackFunc: %@", s_callbackGameObject, s_callbackFunc);
84 | #endif
85 | }
86 |
87 | /// Step 1: Requests localized information about a product from the Apple App Store.
88 | /// Step 2: Adds payment of the product with the given product identifier.
89 | void DYFRetrieveProductFromAppStore(const char* productId)
90 | {
91 | if (![DYFStore canMakePayments]) {
92 | MKVContainer *item = [MKVContainer container];
93 | [item setValue:@"This device is not able or allowed to make payments!" forKey:@"err_desc"];
94 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_CANNOT_MAKE_PAYMENTS, item);
95 | return;
96 | }
97 |
98 | NSString *productIdentifier = __OBJC_STRING(productId);
99 | [DYFStore.defaultStore requestProductWithIdentifier:productIdentifier success:^(NSArray *products, NSArray *invalidIdentifiers) {
100 | if (products.count == 1) {
101 | NSString *productId = ((SKProduct *)products[0]).productIdentifier;
102 | MKVContainer *item = [MKVContainer container];
103 | [item setValue:productId forKey:@"p_id"];
104 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_GET_PRODUCT_SUCCESSFULLY, item);
105 | } else {
106 | // There is no this product for sale!
107 | MKVContainer *item = [MKVContainer container];
108 | [item setValue:@"There is no this product for sale!" forKey:@"err_desc"];
109 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_NO_PRODUCT_FOR_SALE, item);
110 | }
111 | } failure:^(NSError *error) {
112 | NSString *value = error.userInfo[NSLocalizedDescriptionKey];
113 | NSString *msg = value ?: error.localizedDescription;
114 |
115 | MKVContainer *item = [MKVContainer container];
116 | [item setValue:@(error.code) forKey:@"err_code"];
117 | [item setValue:msg forKey:@"err_desc"];
118 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_FAIL_TO_GET_PRODUCT, item);
119 | }];
120 | }
121 |
122 | /// Step 1: Requests localized information about a set of products from the Apple App Store.
123 | /// Step 2: After retrieving the localized product list, then display store UI.
124 | /// Step 3: Adds payment of the product with the given product identifier.
125 | void DYFRetrieveProductsFromAppStore(const char* productIds)
126 | {
127 | if (![DYFStore canMakePayments]) {
128 | MKVContainer *item = [MKVContainer container];
129 | [item setValue:@"This device is not able or allowed to make payments!" forKey:@"err_desc"];
130 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_CANNOT_MAKE_PAYMENTS, item);
131 | return;
132 | }
133 |
134 | NSString *jsonForProductIds = __OBJC_STRING(productIds);
135 | NSArray *productIdentifiers = [UnityIAPConnector objectWithJson:jsonForProductIds];
136 | [DYFStore.defaultStore requestProductWithIdentifiers:productIdentifiers success:^(NSArray *products, NSArray *invalidIdentifiers) {
137 | if (products.count > 0) {
138 | NSMutableArray *itemArr = [NSMutableArray array];
139 | for (SKProduct *p in products) {
140 | MKVContainer *item = [MKVContainer container];
141 | [item setValue:p.productIdentifier forKey:@"p_id"];
142 | [item setValue:p.localizedTitle forKey:@"p_title"];
143 | [item setValue:p.price.stringValue forKey:@"p_price"];
144 | [item setValue:UNLocalizedPrice(p) forKey:@"p_localized_price"];
145 | [item setValue:p.localizedDescription forKey:@"p_localized_desc"];
146 | [itemArr addObject:item];
147 | }
148 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_GET_PRODUCTS_SUCCESSFULLY, itemArr);
149 | } else if (products.count == 0 && invalidIdentifiers.count > 0) {
150 | // Please check the product information you set up.
151 | MKVContainer *item = [MKVContainer container];
152 | [item setValue:@"There are no products for sale!" forKey:@"err_desc"];
153 | [item setValue:invalidIdentifiers forKey:@"invalid_ids"];
154 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_NO_PRODUCTS_FOR_SALE, item);
155 | }
156 | } failure:^(NSError *error) {
157 | NSString *value = error.userInfo[NSLocalizedDescriptionKey];
158 | NSString *msg = value ?: error.localizedDescription;
159 |
160 | MKVContainer *item = [MKVContainer container];
161 | [item setValue:@(error.code) forKey:@"err_code"];
162 | [item setValue:msg forKey:@"err_desc"];
163 | UNCallbackMessageDataToUnity(UN_MSG_CBTYPE_FAIL_TO_GET_PRODUCTS, item);
164 | }];
165 | }
166 |
167 | /// Adds payment of the product with the given product identifier, an opaque identifier for the user’s account.
168 | void DYFAddPayment(const char* productId, const char* userId)
169 | {
170 | NSString *productIdentifier = __OBJC_STRING(productId);
171 | NSString *userIdentifier = __OBJC_STRING(userId);
172 | [DYFStoreManager.shared addPayment:productIdentifier userIdentifier:userIdentifier];
173 | }
174 |
175 | /// Restores previously completed purchases with an opaque identifier for the user’s account.
176 | void DYFRestoreTransactions(const char* userId)
177 | {
178 | NSString *userIdentifier = __OBJC_STRING(userId);
179 | [DYFStoreManager.shared restoreTransactions:userIdentifier];
180 | }
181 |
182 | /// Refreshes the App Store receipt in case the receipt is invalid or missing.
183 | void DYFRefreshReceipt()
184 | {
185 | [DYFStoreManager.shared refreshReceipt];
186 | }
187 |
188 | /// Completes a pending transaction.
189 | void DYFFinishTransaction(const char* transactionId, const char* originalTransactionId)
190 | {
191 | NSString *transactionIdentifier = __OBJC_STRING(transactionId);
192 | NSString *orgTransactionIdentifier = __OBJC_STRING(originalTransactionId);
193 |
194 | DYFStore *store = DYFStore.defaultStore;
195 | SKPaymentTransaction *pt = [store extractPurchasedTransaction:transactionIdentifier];
196 | if (pt) {
197 | [DYFStore.defaultStore finishTransaction:pt];
198 | } else {
199 | SKPaymentTransaction *rt = [store extractRestoredTransaction:transactionIdentifier];
200 | [DYFStore.defaultStore finishTransaction:rt];
201 | }
202 | DYFStoreUserDefaultsPersistence *persister = [[DYFStoreUserDefaultsPersistence alloc] init];
203 | [persister removeTransaction:transactionIdentifier];
204 |
205 | if (orgTransactionIdentifier) {
206 | [persister removeTransaction:orgTransactionIdentifier];
207 | }
208 | }
209 |
210 | /// Completes a pending transaction.
211 | void DYFFinishTransaction_(const char* transactionId)
212 | {
213 | DYFFinishTransaction(transactionId, "");
214 | }
215 |
216 | /// Queries those incompleted transactions from keychain.
217 | void DYFQueryIncompletedTransactions()
218 | {
219 | [DYFStoreManager.shared queryIncompletedTransactions];
220 | }
221 |
222 | #if defined(_cplusplus)
223 | }
224 | #endif
225 |
226 | @implementation UnityIAPConnector
227 |
228 | + (NSString *)jsonWithObject:(id)object
229 | {
230 | if ([NSJSONSerialization isValidJSONObject:object]) {
231 | NSError *error;
232 | NSData *data = [NSJSONSerialization dataWithJSONObject:object options:kNilOptions error:&error];
233 | if (!error) {
234 | return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
235 | }
236 | #if DEBUG
237 | NSLog(@"[IAPConnector] error: %@", error.description);
238 | #endif
239 | }
240 |
241 | return nil;
242 | }
243 |
244 | + (id)objectWithJson:(NSString *)json
245 | {
246 | if (json && json.length > 0) {
247 | NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
248 | NSError *error;
249 | id obj = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
250 | if (!error) {
251 | return obj;
252 | }
253 | #if DEBUG
254 | NSLog(@"[IAPConnector] error: %@", error.description);
255 | #endif
256 | }
257 | return nil;
258 | }
259 |
260 | @end
261 |
262 | @implementation NSMutableDictionary (UNTypeDef)
263 |
264 | + (instancetype)container
265 | {
266 | return [NSMutableDictionary dictionary];
267 | }
268 |
269 | @end
270 |
--------------------------------------------------------------------------------
/unity-iap/unity/UnityIAPManager.cs:
--------------------------------------------------------------------------------
1 | ///
2 | /// File Name: UnityIAPManager.cs
3 | ///
4 | /// Author: Tenfay
5 | ///
6 | /// Brief:
7 | /// Unity implements Apple's in-app purchases for iOS.
8 | ///
9 | /// Log:
10 | /// 1. created, 2020-04-16, Tenfay.
11 | ///
12 |
13 | using UnityEngine;
14 | using System.Collections;
15 | using System.Runtime.InteropServices;
16 | using Newtonsoft.Json;
17 | using Newtonsoft.Json.Linq;
18 | using System.Collections.Generic;
19 |
20 | // Note: You need to change "com.dl.core.SingletonObject" to your's package.
21 | public class UnityIAPManager : com.dl.core.SingletonObject
22 | {
23 |
24 | protected override void Spawn ()
25 | {
26 | base.Spawn ();
27 | this.gameObject.name = "Main";
28 | }
29 |
30 | public enum CallbackType
31 | {
32 | Action_CannotMakePayments = 1, // The device is not able or allowed to make payments.
33 | Action_GetProductSuccessfully = 2, // The product has been got successfully.
34 | Action_FailToGetProduct = 3, // The product has failed to be got.
35 | Action_NoProductForSale = 4, // There is no product for sale.
36 | Action_GetProductsSuccessfully = 5, // A set of products have been got successfully.
37 | Action_FailToGetProducts = 6, // A set of products have failed to be got.
38 | Action_NoProductsForSale = 7, // There is no products for sale.
39 | Action_PurchaseDeferred = 8, // The purchase has been deferred.
40 | Action_PurchaseInProgress = 9, // The purchase is in progress.
41 | Action_PurchaseCancelled = 10, // The purchase has been cancelled by user.
42 | Action_PurchaseFailed = 11, // The purchase has failed.
43 | Action_PurchaseSucceeded = 12, // The purchase is successful.
44 | Action_FailToRestorePurchase = 13, // The purchase has failed to be restored.
45 | Action_PurchaseRestored = 14, // The purchase has been restored successfully.
46 | Action_RefreshReceipt = 15, // The receipt needs to be refreshed.
47 | Action_FailToRefreshReceipt = 16, // The receipt has failed to be refreshed.
48 | Action_IncompletedTransactions = 17 // The incompleted transactions were queried, then continue to verify receipt.
49 | }
50 |
51 | public static bool canMakePayments = false;
52 |
53 | #if !UNITY_EDITOR && UNITY_IOS
54 |
55 | // Initializes message callback for Unity.
56 | [DllImport("__Internal")]
57 | private static extern void DYFInitUnityMsgCallback(string gameObject, string func);
58 |
59 | // Retrieves a product from App Store and adds the payment.
60 | [DllImport("__Internal")]
61 | private static extern void DYFRetrieveProductFromAppStore(string productId);
62 |
63 | // Retrieves a set of products from App Store and displays store UI, then add the payment.
64 | [DllImport("__Internal")]
65 | private static extern void DYFRetrieveProductsFromAppStore(string jsonForProductIds);
66 |
67 | // Adds the payment of a product.
68 | [DllImport("__Internal")]
69 | private static extern void DYFAddPayment(string productId, string userId);
70 |
71 | // Restores previously completed purchases.
72 | [DllImport("__Internal")]
73 | private static extern void DYFRestoreTransactions(string userId);
74 |
75 | // Refreshes the App Store receipt in case the receipt is invalid or missing.
76 | [DllImport("__Internal")]
77 | private static extern void DYFRefreshReceipt();
78 |
79 | // Completes a pending transaction.
80 | [DllImport("__Internal")]
81 | private static extern void DYFFinishTransaction(string transactionId, string originalTransactionId);
82 |
83 | // Completes a pending transaction.
84 | [DllImport("__Internal")]
85 | private static extern void DYFFinishTransaction_(string transactionId);
86 |
87 | // Queries those incompleted transactions.
88 | [DllImport("__Internal")]
89 | private static extern void DYFQueryIncompletedTransactions();
90 |
91 | #endif
92 |
93 | // Note: You can get the array from your server or write the fixed values.
94 | private JArray getJArrayOfProductIds()
95 | {
96 | JArray a = new JArray();
97 | a.Add("com.xx.gm.pack1");
98 | a.Add("com.xx.gm.pack2");
99 | a.Add("com.xx.gm.pack3");
100 | a.Add("com.xx.gm.pack4");
101 | a.Add("com.xx.gm.pack5");
102 | a.Add("com.xx.gm.pack6");
103 | // ......
104 | return a;
105 | }
106 |
107 | // Note: You can get a json string from a jarray.
108 | private string getJsonOfProductIds()
109 | {
110 | JArray a = getJArrayOfProductIds();
111 | string json = JsonConvert.SerializeObject(a);
112 | return json;
113 | }
114 |
115 | public void initUnityMsgCallback(string gameObject, string func)
116 | {
117 | LogManager.Log("initUnityMsgCallback=" + gameObject + "," + func);
118 | #if !UNITY_EDITOR && UNITY_IOS
119 | // UMessageCallback(string msg)
120 | DYFInitUnityMsgCallback(gameObject, func);
121 | #endif
122 | }
123 |
124 | public void retrieveProduct(string productId)
125 | {
126 | if (Application.platform != RuntimePlatform.OSXEditor) {
127 | LogManager.Log("retrieveProduct=" + productId);
128 | #if !UNITY_EDITOR && UNITY_IOS
129 | // Tips: show loading panel.
130 | DYFRetrieveProductFromAppStore(productId);
131 | #endif
132 | }
133 | }
134 |
135 | public void retrieveProducts()
136 | {
137 | if (Application.platform != RuntimePlatform.OSXEditor) {
138 | string jsonOfProductIds = getJsonOfProductIds()
139 | LogManager.Log("retrieveProducts=" + jsonOfProductIds);
140 | #if !UNITY_EDITOR && UNITY_IOS
141 | // Tips: show loading panel.
142 | DYFRetrieveProductsFromAppStore(jsonOfProductIds);
143 | #endif
144 | }
145 | }
146 |
147 | public void addPayment(string productId, string userId)
148 | {
149 | if (Application.platform != RuntimePlatform.OSXEditor) {
150 | LogManager.Log("addPayment=" + productId + "," + userId);
151 | #if !UNITY_EDITOR && UNITY_IOS
152 | // Tips: show loading panel.
153 | DYFAddPayment(productId, userId);
154 | #endif
155 | }
156 | }
157 |
158 | public void restoreTransactions(string userId)
159 | {
160 | if (Application.platform != RuntimePlatform.OSXEditor) {
161 | LogManager.Log("DYFRestoreTransactions=", userId);
162 | #if !UNITY_EDITOR && UNITY_IOS
163 | // Tips: show loading panel.
164 | DYFRestoreTransactions(userId);
165 | #endif
166 | LogManager.Log("Store start restoring completed transactions...", LogType.Normal);
167 | }
168 | }
169 |
170 | public void refreshReceipt()
171 | {
172 | if (Application.platform != RuntimePlatform.OSXEditor) {
173 | #if !UNITY_EDITOR && UNITY_IOS
174 | // Tips: show loading panel.
175 | DYFRefreshReceipt();
176 | #endif
177 | LogManager.Log("Store start refreshing receipt...", LogType.Normal);
178 | }
179 | }
180 |
181 | public void finishTransaction(string transactionId, string originalTransactionId)
182 | {
183 | if (Application.platform != RuntimePlatform.OSXEditor) {
184 | LogManager.Log("finishTransaction", LogType.Normal);
185 | #if !UNITY_EDITOR && UNITY_IOS
186 | DYFFinishTransaction(transactionId, originalTransactionId);
187 | #endif
188 | }
189 | }
190 |
191 | public void finishTransaction_(string transactionId)
192 | {
193 | if (Application.platform != RuntimePlatform.OSXEditor) {
194 | LogManager.Log("finishTransaction_", LogType.Normal);
195 | #if !UNITY_EDITOR && UNITY_IOS
196 | DYFFinishTransaction_(transactionId);
197 | #endif
198 | }
199 | }
200 |
201 | public void queryIncompletedTransactions()
202 | {
203 | if (Application.platform != RuntimePlatform.OSXEditor) {
204 | LogManager.Log("queryIncompletedTransactions", LogType.Normal);
205 | #if !UNITY_EDITOR && UNITY_IOS
206 | DYFQueryIncompletedTransactions();
207 | #endif
208 | }
209 | }
210 |
211 | public void UMessageCallback(string msg)
212 | {
213 | LogManager.Log ("UMessageCallback: " + msg, LogType.Normal);
214 |
215 | if (msg.Length == 0) {
216 | // You need to present a panel to prompt the user.
217 | // Tips: The data occurs an error.
218 | return;
219 | }
220 |
221 | try {
222 | JObject json = (JObject)JsonConvert.DeserializeObject(msg);
223 | int act = -1;
224 | if (int.TryParse(json["msg_code"].ToString (), out act)) {
225 |
226 | switch (act) {
227 | case (int)CallbackType.Action_CannotMakePayments:
228 | {
229 | LogManager.Log ("CallbackType.Action_CannotMakePayments");
230 | // Tips: The device is not able or allowed to make payments.
231 | string err_desc = (string)json["msg_data"]["err_desc"];
232 | LogManager.Log("err_desc=", err_desc, LogType.Error);
233 | // You need to present a panel to prompt the user.
234 | break;
235 | }
236 |
237 | case (int)CallbackType.Action_GetProductSuccessfully:
238 | {
239 | LogManager.Log ("CallbackType.Action_GetProductSuccessfully");
240 | string productId = (string)json["msg_data"]["p_id"];
241 | // You get this from your user system when you need it.
242 | string userId = null;
243 | addPayment(productId, userId);
244 | break;
245 | }
246 |
247 | case (int)CallbackType.Action_FailToGetProduct:
248 | {
249 | LogManager.Log ("CallbackType.Action_FailToGetProduct");
250 |
251 | // Tips: The product has failed to be got.
252 | string err_code = json["msg_data"]["err_desc"].ToString();
253 | string err_desc = (string)json["msg_data"]["err_desc"];
254 | LogManager.Log("err_desc=", err_code, err_desc, LogType.Error);
255 | // You need to present a panel to prompt the user.
256 |
257 | break;
258 | }
259 |
260 | case (int)CallbackType.Action_NoProductForSale:
261 | {
262 | LogManager.Log ("CallbackType.Action_NoProductForSale");
263 | // Tips: There is no product for sale.
264 | string err_desc = (string)json["msg_data"]["err_desc"];
265 | LogManager.Log("err_desc=", err_desc, LogType.Error);
266 | // You need to present a panel to prompt the user.
267 | break;
268 | }
269 |
270 | case (int)CallbackType.Action_GetProductsSuccessfully:
271 | {
272 | LogManager.Log ("CallbackType.Action_GetProductsSuccessfully");
273 | JArray arr = JArray.Parse(json["msg_data"].ToString());
274 | parseProductList(arr);
275 | break;
276 | }
277 |
278 | case (int)CallbackType.Action_FailToGetProducts:
279 | {
280 | LogManager.Log ("CallbackType.Action_FailToGetProducts");
281 | // Tips: A set of products have failed to be got.
282 | string err_code = json["msg_data"]["err_desc"].ToString();
283 | string err_desc = (string)json["msg_data"]["err_desc"];
284 | LogManager.Log("err_desc=", err_code, err_desc, LogType.Error);
285 | // You need to present a panel to prompt the user.
286 | break;
287 | }
288 |
289 | case(int)CallbackType.Action_NoProductsForSale:
290 | {
291 | LogManager.Log ("CallbackType.Action_NoProductsForSale");
292 | // Tips: There is no products for sale.
293 | string err_desc = (string)json["msg_data"]["err_desc"];
294 | string invalid_ids = (string)json["msg_data"]["invalid_ids"];
295 | LogManager.Log("err_desc=", err_desc, invalid_ids, LogType.Error);
296 | // You need to present a panel to prompt the user.
297 | break;
298 | }
299 |
300 | case(int)CallbackType.Action_PurchaseDeferred:
301 | {
302 | LogManager.Log ("CallbackType.Action_PurchaseDeferred");
303 |
304 | // Tips: The purchase has been deferred.
305 | string desc = (string)json["msg_data"]["m_desc"];
306 | LogManager.Log("err_desc=", desc);
307 | // Yon can choose to process the program.
308 |
309 | break;
310 | }
311 |
312 | case(int)CallbackType.Action_PurchaseInProgress:
313 | {
314 | LogManager.Log ("CallbackType.Action_PurchaseInProgress");
315 | // Tips: The purchase is in progress.
316 | string desc = (string)json["msg_data"]["m_desc"];
317 | LogManager.Log("err_desc=", desc);
318 | // You can present a panel to prompt the user.
319 | break;
320 | }
321 |
322 | case(int)CallbackType.Action_PurchaseCancelled:
323 | {
324 | LogManager.Log ("CallbackType.Action_PurchaseCancelled");
325 | // Tips: The purchase has been cancelled by user.
326 | string desc = (string)json["msg_data"]["m_desc"];
327 | LogManager.Log("err_desc=", desc);
328 | // You need to present a panel to prompt the user.
329 | break;
330 | }
331 |
332 | case(int)CallbackType.Action_PurchaseFailed:
333 | {
334 | LogManager.Log ("CallbackType.Action_PurchaseFailed");
335 | // Tips: The purchase has failed.
336 | string err_code = json["msg_data"]["err_desc"].ToString();
337 | string err_desc = (string)json["msg_data"]["err_desc"];
338 | LogManager.Log("err_desc=", err_code, err_desc, LogType.Error);
339 | // You need to present a panel to prompt the user.
340 | break;
341 | }
342 |
343 | case(int)CallbackType.Action_PurchaseSucceeded:
344 | {
345 | LogManager.Log ("CallbackType.Action_PurchaseSucceeded");
346 | // Tips: The purchase has been completed.
347 | JObject obj = JObject.Parse(json["msg_data"].ToString());
348 | // You can verify the receipt.
349 | verifyReceipt(obj);
350 | break;
351 | }
352 |
353 | case(int)CallbackType.Action_FailToRestorePurchase:
354 | {
355 | LogManager.Log ("CallbackType.Action_FailToRestorePurchase");
356 | // Tips: The purchase has failed to be restored.
357 | string err_code = json["msg_data"]["err_desc"].ToString();
358 | string err_desc = (string)json["msg_data"]["err_desc"];
359 | LogManager.Log("err_desc=", err_code, err_desc, LogType.Error);
360 | // You need to present a panel to prompt the user.
361 | break;
362 | }
363 |
364 | case(int)CallbackType.Action_PurchaseRestored:
365 | {
366 | LogManager.Log ("CallbackType.Action_PurchaseRestored");
367 | // Tips: The purchase has been restored successfully.
368 | JObject obj = JObject.Parse(json["msg_data"].ToString());
369 | // You can verify the receipt.
370 | verifyReceipt(obj);
371 | break;
372 | }
373 |
374 | case(int)CallbackType.Action_RefreshReceipt:
375 | {
376 | LogManager.Log ("CallbackType.Action_RefreshReceipt");
377 | // Tips: The receipt needs to be refreshed.
378 | string desc = (string)json["msg_data"]["m_desc"];
379 | LogManager.Log("err_desc=", desc);
380 | refreshReceipt()
381 | break;
382 | }
383 |
384 | case(int)CallbackType.Action_FailToRefreshReceipt:
385 | {
386 | LogManager.Log ("CallbackType.Action_FailToRefreshReceipt");
387 | // Tips: The receipt has failed to be refreshed.
388 | string err_code = json["msg_data"]["err_desc"].ToString();
389 | string err_desc = (string)json["msg_data"]["err_desc"];
390 | LogManager.Log("err_desc=", err_code, err_desc, LogType.Error);
391 | // You need to present a panel to inform the user to refresh receipt again.
392 | // If this is not done, this transaction will not be completed.
393 | // refreshReceipt()
394 | break;
395 | }
396 |
397 | case(int)CallbackType.Action_IncompletedTransactions:
398 | {
399 | LogManager.Log ("CallbackType.Action_IncompletedTransactions");
400 | // Tips: The receipt has failed to be refreshed.
401 | string data = json["msg_data"].ToString();
402 | if (data.Length != 0) {
403 | LogManager.Log("data=", data, LogType.Normal);
404 | JArray arr = JArray.Parse(data)
405 | for(int i = 0; i < arr.Count; i++) {
406 | JObject jo = JObject.Parse(jarr[i].ToString());
407 | int state = int.Parse(jo["t_state"].ToString);
408 | string productId = jo["p_id"].ToString();
409 | string userId = jo["u_id"].ToString();
410 | string transId = jo["t_id"].ToString();
411 | string transTimestamp = jo["t_ts"].ToString();
412 | string orgTransId = jo["orgt_id"].ToString();
413 | string orgTransTimestamp = jo["orgt_ts"].ToString();
414 | string base64EncodedReceipt = jo["t_receipt"].ToString();
415 | requestToVerifyReceipt(productId, transId, base64EncodedReceipt, userId, transTimestamp, orgTransId, orgTransTimestamp);
416 | }
417 | }
418 | break;
419 | }
420 |
421 | default:
422 | break;
423 | }
424 | }
425 | } catch (System.Exception e) {
426 | LogManager.Log ("exception=" + e.ToString (), LogType.Fatal);
427 | }
428 | }
429 |
430 | private void parseProductList(JArray jarr)
431 | {
432 | try {
433 | LogManager.Log ("parseProductList... jarr: " + jarr.ToString());
434 |
435 | for(int i = 0; i < jarr.Count; i++) {
436 | JObject jo = JObject.Parse(jarr[i].ToString());
437 | string productId = jo["p_id"].ToString();
438 | string title = jo["p_title"].ToString();
439 | string price = jo["p_price"].ToString();
440 | string localizedPrice = jo["p_localized_price"].ToString();
441 | string localizedDesc = jo["p_localized_desc"].ToString();
442 |
443 | LogManager.Log ("ProductList..." + i.ToString() + " productId: " + productId +
444 | "; title: " + title + "; price: " + price + "; localizedPrice: " +
445 | localizedPrice + "; localizedDesc: " + localizedDesc);
446 | }
447 | // Call displayStorePanel(), The parameters need to be defined by you.
448 | // Waiting for the user to choose to buy goods, then call addPayment(...)
449 | } catch (System.Exception e) {
450 | LogManager.Log (e.ToString (), LogType.Fatal);
451 | }
452 | }
453 |
454 | private void displayStorePanel()
455 | {
456 | // After getting the products, then the store panel is displayed.
457 | }
458 |
459 | private void verifyReceipt(JObject jo)
460 | {
461 | try {
462 | LogManager.Log ("verifyReceipt... jo: " + jo.ToString());
463 | int state = int.Parse(jo["t_state"].ToString);
464 | string productId = jo["p_id"].ToString();
465 | string userId = jo["u_id"].ToString();
466 | string transId = jo["t_id"].ToString();
467 | string transTimestamp = jo["t_ts"].ToString();
468 | string orgTransId = jo["orgt_id"].ToString();
469 | string orgTransTimestamp = jo["orgt_ts"].ToString();
470 | string base64EncodedReceipt = jo["t_receipt"].ToString();
471 |
472 | // You can also add the bundle identifier.
473 | requestToVerifyReceipt(productId, transId, base64EncodedReceipt, userId, transTimestamp, orgTransId, orgTransTimestamp);
474 | } catch (System.Exception e) {
475 | LogManager.Log (e.ToString (), LogType.Fatal);
476 | }
477 | }
478 |
479 | private void requestToVerifyReceipt(string productId, string transId, string base64EncodedReceipt,
480 | string userId, string transTimestamp, string orgTransId, string orgTransTimestamp) {
481 | // The URL for receipt verification.
482 | // Sandbox: "https://sandbox.itunes.apple.com/verifyReceipt"
483 | // Production: "https://buy.itunes.apple.com/verifyReceipt"
484 |
485 | // Finally, you call this method to complete the transaction.
486 | // finishTransaction(transactionId); finishTransaction(orgTransactionId);
487 |
488 | // Recommended reference links:
489 | // https://itenfay.github.io/2016/10/16/in-app-purchase-complete-programming-guide-for-iOS/
490 | // https://itenfay.github.io/2016/10/12/how-to-easily-complete-in-app-purchase-configuration-for-iOS/
491 | // https://www.jianshu.com/p/de030cd6e4a3
492 | // https://www.jianshu.com/p/1875e0c7ac5d
493 |
494 | // Performs http request or builds tcp/udp connection.
495 | }
496 |
497 | }
498 |
--------------------------------------------------------------------------------