├── .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 MIT](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](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 MIT](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](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 | --------------------------------------------------------------------------------