Store.cpp 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. /******************************************************************************
  2. For 'callback' pass temporary 'Purchase' object, and not from '_purchases',
  3. this is in case user would call 'consume' inside the callback,
  4. which would remove it from container making the reference invalid.
  5. /******************************************************************************/
  6. #include "stdafx.h"
  7. #include "../Platforms/iOS/iOS.h"
  8. #if WINDOWS_NEW
  9. using namespace concurrency;
  10. #endif
  11. #if DEBUG
  12. #define WIN_STORE Windows::ApplicationModel::Store::CurrentAppSimulator
  13. #else
  14. #define WIN_STORE Windows::ApplicationModel::Store::CurrentApp
  15. #endif
  16. namespace EE{
  17. /******************************************************************************/
  18. PlatformStore Store;
  19. /******************************************************************************/
  20. static void ConvertDate(Str text, DateTime &date)
  21. {
  22. // sample text "2016-03-08T04:49:36Z"
  23. text.replace('T', ' ').replace('Z', '\0');
  24. date.fromText(text);
  25. }
  26. /******************************************************************************/
  27. static Bool BackgroundUpdate(Thread &thread) {return ((PlatformStore*)thread.user)->backgroundUpdate();}
  28. Bool PlatformStore::backgroundUpdate()
  29. {
  30. if(_get_item_details.elms()) // want to get item details
  31. {
  32. SyncLockerEx locker(_lock);
  33. if(_get_item_details.elms())
  34. {
  35. Memc<Str> item_ids; Swap(item_ids, _get_item_details); locker.off();
  36. #if ANDROID
  37. JNI jni;
  38. if(jni && ActivityClass)
  39. if(JMethodID getItemDetails=jni->GetStaticMethodID(ActivityClass, "getItemDetails", "(ZZIJJ)Z"))
  40. {
  41. Memt <Item> items;
  42. MemPtr<Item> items_ptr=items;
  43. MemPtr<Str > item_ids_ptr=item_ids;
  44. Bool ok=jni->CallStaticBooleanMethod(ActivityClass, getItemDetails, jboolean(supportsItems()), jboolean(supportsSubscriptions()), jint(item_ids_ptr.elms()), jlong(&item_ids_ptr), jlong(&items_ptr));
  45. if(items.elms())
  46. {
  47. locker.on();
  48. REPA(items)Swap(_new_items.New(), items[i]);
  49. locker.off();
  50. }
  51. }
  52. #endif
  53. }
  54. return true; // keep thread alive
  55. }
  56. if(_refresh_purchases) // want to get active purchases
  57. {
  58. SyncLockerEx locker(_lock);
  59. if(_refresh_purchases)
  60. {
  61. locker.off();
  62. #if ANDROID
  63. JNI jni;
  64. if(jni && ActivityClass)
  65. if(JMethodID getPurchases=jni->GetStaticMethodID(ActivityClass, "getPurchases", "(ZJ)Z"))
  66. {
  67. Memc <Purchase> purchases;
  68. MemPtr<Purchase> purchases_ptr=purchases;
  69. Bool regular=(supportsItems () && jni->CallStaticBooleanMethod(ActivityClass, getPurchases, jboolean(false), jlong(&purchases_ptr)));
  70. Bool subs =(supportsSubscriptions() && jni->CallStaticBooleanMethod(ActivityClass, getPurchases, jboolean(true ), jlong(&purchases_ptr)));
  71. locker.on();
  72. Swap(_new_purchases, purchases);
  73. _has_new_purchases=true;
  74. locker.off();
  75. }
  76. #endif
  77. _refresh_purchases=false; // disable at the end because this is just a bool, doesn't require 'SyncLocker'
  78. }
  79. return true; // keep thread alive
  80. }
  81. if(_consume.elms()) // want to consume purchases
  82. {
  83. SyncLockerEx locker(_lock);
  84. if(_consume.elms()) // want to consume purchases
  85. {
  86. Processed purchase; Swap(purchase.token, _consume.first()); _consume.remove(0, true);
  87. if(C Purchase *existing=findPurchaseByToken(purchase.token))SCAST(Purchase, purchase)=*existing;
  88. locker.off();
  89. #if ANDROID
  90. JNI jni;
  91. if(jni && ActivityClass)
  92. if(JMethodID consume=jni->GetStaticMethodID(ActivityClass, "consume", "(Ljava/lang/String;)I"))
  93. if(JString j_token=JString(jni, purchase.token))
  94. {
  95. purchase.result=(RESULT)jni->CallStaticIntMethod(ActivityClass, consume, j_token());
  96. locker.on();
  97. Swap(_processed.New(), purchase);
  98. locker.off();
  99. }
  100. #endif
  101. }
  102. return true; // keep thread alive
  103. }
  104. return false;
  105. }
  106. /******************************************************************************/
  107. PlatformStore::~PlatformStore()
  108. {
  109. _thread.del(); // delete the thread before anything else
  110. #if IOS
  111. [IAPMgr release]; IAPMgr=null;
  112. #endif
  113. }
  114. PlatformStore::PlatformStore()
  115. {
  116. callback=null;
  117. _supports_items=_supports_subs=_has_new_purchases=_refresh_purchases=false;
  118. #if WINDOWS_NEW
  119. try // exception may occur when using 'CurrentApp' instead of 'CurrentAppSimulator' on a debug build
  120. {
  121. _supports_items=(WIN_STORE::LicenseInformation->IsActive && !WIN_STORE::LicenseInformation->IsTrial); // purchases are unavailable for trial according to this page, https://msdn.microsoft.com/en-us/windows/uwp/monetize/enable-in-app-product-purchases "In-app products cannot be offered from a trial version of an app"
  122. }
  123. catch(...){}
  124. // list purchases
  125. #if 0 // can't do this because 'token' is unavailable in this method, instead 'refreshPurchases' needs to be used
  126. auto product=WIN_STORE::LicenseInformation->ProductLicenses->First();
  127. REP(WIN_STORE::LicenseInformation->ProductLicenses->Size)
  128. {
  129. if(Bool active=product->Current->Value->IsActive)
  130. {
  131. //Str key=product->Current->Key->Data(); this is the same as 'id'
  132. Purchase &purchase=_purchases.New();
  133. purchase.id=product->Current->Value->ProductId->Data();
  134. //purchase.token=; // unknown
  135. purchase.date.zero(); // unknown
  136. }
  137. product->MoveNext();
  138. }
  139. #endif
  140. #elif IOS
  141. if(IAPMgr=[[IAPManager alloc] init])
  142. {
  143. _supports_items=_supports_subs=[SKPaymentQueue canMakePayments];
  144. [[SKPaymentQueue defaultQueue] addTransactionObserver:IAPMgr];
  145. //[[SKPaymentQueue defaultQueue] transactions] are always empty here, so there's no point in listing them
  146. }
  147. #endif
  148. }
  149. C PlatformStore::Item * PlatformStore::findItem (C Str &item_id)C {if(item_id.is())REPA(_items )if(Equal(_items [i].id , item_id, true))return &_items [i]; return null;}
  150. C PlatformStore::Purchase* PlatformStore::findPurchase (C Str &item_id)C {if(item_id.is())REPA(_purchases)if(Equal(_purchases[i].id , item_id, true))return &_purchases[i]; return null;}
  151. C PlatformStore::Purchase* PlatformStore::findPurchaseByToken(C Str &token )C {if(token .is())REPA(_purchases)if(Equal(_purchases[i].token, token , true))return &_purchases[i]; return null;}
  152. static void Update(PlatformStore &store) {store.update();}
  153. void PlatformStore::update()
  154. {
  155. // del thread
  156. if(_thread.created() && !_thread.active()) // delete thread if it's no longer active to free system resources
  157. {
  158. SyncLocker locker(_lock);
  159. if(_thread.created() && !_thread.active())_thread.del();
  160. }
  161. if(_thread.created())App._callbacks.include(Update, T); // if still created then call the callback again later
  162. // process updates
  163. if(_new_items.elms())
  164. {
  165. SyncLockerEx locker(_lock);
  166. if(_new_items.elms())
  167. {
  168. REPA(_new_items)
  169. {
  170. Item &src=_new_items[i];
  171. Item *dest=ConstCast(findItem(src.id)); if(!dest)dest=&_items.New();
  172. Swap(src, *dest);
  173. }
  174. _new_items.clear();
  175. locker.off();
  176. if(callback)callback(REFRESHED_ITEMS, null);
  177. }
  178. }
  179. if(_has_new_purchases) // test bool because purchases container may be empty when it was received
  180. {
  181. SyncLockerEx locker(_lock);
  182. if(_has_new_purchases)
  183. {
  184. _has_new_purchases=false;
  185. Swap(_purchases, _new_purchases); _new_purchases.clear();
  186. locker.off();
  187. if(callback)callback(REFRESHED_PURCHASES, null);
  188. }
  189. }
  190. if(_processed.elms())
  191. {
  192. SyncLockerEx locker(_lock);
  193. REPA(_processed)
  194. {
  195. Processed &purchase=_processed[i];
  196. C Purchase *existing=findPurchaseByToken(purchase.token);
  197. switch(purchase.result) // first add/remove to list of purchases
  198. {
  199. case PlatformStore::CONSUMED :
  200. case PlatformStore::REFUND : _purchases.removeData(existing, true); break; // remove
  201. case PlatformStore::PURCHASED: if(!existing)_purchases.add(purchase); break; // add
  202. }
  203. if(callback)callback(purchase.result, &purchase); // !! here don't pass purchase from '_purchases' !!
  204. }
  205. _processed.clear();
  206. }
  207. }
  208. Bool PlatformStore::refreshItems(C MemPtr<Str> &item_ids)
  209. {
  210. if(item_ids.elms())
  211. {
  212. #if WINDOWS_NEW
  213. try // exception may occur when using 'CurrentApp' instead of 'CurrentAppSimulator' on a debug build
  214. {
  215. #if 0 // doc says this is Windows Phone only, on Desktop it crashes, so don't bother
  216. Platform::Collections::Vector<Platform::String^> ^product_ids = ref new Platform::Collections::Vector<Platform::String^>(item_ids.elms());
  217. FREPA(item_ids)product_ids->SetAt(i, ref new Platform::String(item_ids[i]));
  218. create_task(WIN_STORE::LoadListingInformationByProductIdsAsync(product_ids)).then([](Windows::ApplicationModel::Store::ListingInformation ^listing)
  219. #elif 1 // this works ok but returns all items
  220. create_task(WIN_STORE::LoadListingInformationAsync()).then([](Windows::ApplicationModel::Store::ListingInformation ^listing)
  221. #endif
  222. {
  223. // this will be called on the main thread
  224. if(listing)
  225. {
  226. /*Str name=listing->Name->Data(),
  227. desc=listing->Description->Data(),
  228. price=listing->FormattedPrice->Data(),
  229. market=listing->CurrentMarket->Data();*/
  230. // even though we're getting full list of items and we could first delete existing 'Store._items', don't do that for thread-safety in case secondary threads already operate on existing items
  231. if(listing->ProductListings)
  232. {
  233. auto product=listing->ProductListings->First();
  234. REP(listing->ProductListings->Size)
  235. {
  236. //Str key=product->Current->Key->Data(); this is the same as 'id'
  237. Str id =product->Current->Value->ProductId->Data();
  238. PlatformStore::Item *item=ConstCast(Store.findItem(id)); if(!item)item=&Store._items.New();
  239. item->subscription=false;
  240. item->id =id;
  241. item->name =product->Current->Value->Name->Data();
  242. item->desc =product->Current->Value->Description->Data();
  243. item->price=product->Current->Value->FormattedPrice->Data();
  244. product->MoveNext();
  245. }
  246. if(Store.callback)Store.callback(PlatformStore::REFRESHED_ITEMS, null);
  247. }
  248. }
  249. });
  250. }
  251. catch(...){return false;}
  252. return true;
  253. #elif ANDROID
  254. if(supportsItems() || supportsSubscriptions())
  255. {
  256. Bool added=false;
  257. SyncLocker locker(_lock);
  258. REPA(item_ids)
  259. {
  260. C Str &item_id=item_ids[i];
  261. REPA(_get_item_details)if(Equal(_get_item_details[i], item_id, true))goto has;
  262. _get_item_details.add(item_id); added=true;
  263. has:;
  264. }
  265. if(_get_item_details.elms() && !_thread.active()){_thread.create(BackgroundUpdate, this); App._callbacks.include(Update, T);}
  266. return true;
  267. }
  268. return false;
  269. #elif IOS
  270. if(IAPMgr)
  271. if(NSMutableArray *array=[NSMutableArray arrayWithCapacity:item_ids.elms()])
  272. {
  273. FREPA(item_ids)[array addObject:NSStringAuto(item_ids[i])];
  274. SKProductsRequest *pr=[[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:array]];
  275. pr.delegate=IAPMgr;
  276. [pr start];
  277. //[pr release]; don't release here, instead this is released in 'IAPManager.didReceiveResponse'
  278. //[array release]; // 'arrayWithCapacity' shouldn't be manually released
  279. return true;
  280. }
  281. return false;
  282. #endif
  283. }
  284. return true;
  285. }
  286. Bool PlatformStore::refreshPurchases()
  287. {
  288. #if WINDOWS_NEW
  289. try // exception may occur when using 'CurrentApp' instead of 'CurrentAppSimulator' on a debug build
  290. {
  291. create_task(WIN_STORE::GetAppReceiptAsync()).then([](Platform::String ^receipt)
  292. {
  293. // this will be called on the main thread
  294. if(receipt)
  295. {
  296. C wchar_t *r=receipt->Data();
  297. FileText f; f.readMem(r, Length(r)*SIZE(*r), UTF_16); // use 'UTF_16' because 'r' is a 16-bit string
  298. XmlData xml; xml.load(f);
  299. if(XmlNode *receipt=xml.findNode("Receipt"))
  300. {
  301. Memc<Purchase> purchases; FREPA(receipt->nodes)
  302. {
  303. XmlNode &node=receipt->nodes[i]; if(node.name=="ProductReceipt")
  304. {
  305. UID token_id;
  306. if(XmlParam *token=node.findParam("Id"))if(token_id.fromCanonical(token->value)) // token in guid format
  307. if(XmlParam *id =node.findParam("ProductId"))
  308. {
  309. Purchase &purchase=purchases.New();
  310. purchase.id =id->value;
  311. purchase.token=token_id.asHex();
  312. purchase.date.zero(); if(XmlParam *date=node.findParam("PurchaseDate"))ConvertDate(date->value, purchase.date);
  313. }
  314. }
  315. }
  316. Swap(Store._purchases, purchases);
  317. }
  318. if(Store.callback)Store.callback(REFRESHED_PURCHASES, null);
  319. }
  320. });
  321. }
  322. catch(...){return false;}
  323. #elif ANDROID
  324. if(supportsItems() || supportsSubscriptions())
  325. {
  326. if(!_refresh_purchases)
  327. {
  328. SyncLocker locker(_lock);
  329. if(!_refresh_purchases)
  330. {
  331. _refresh_purchases=true;
  332. if(!_thread.active()){_thread.create(BackgroundUpdate, this); App._callbacks.include(Update, T);}
  333. }
  334. }
  335. return true;
  336. }
  337. return false;
  338. #elif IOS
  339. // there's no such method for Apple
  340. #endif
  341. return true;
  342. }
  343. PlatformStore& PlatformStore::restorePurchases()
  344. {
  345. #if IOS
  346. [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
  347. #endif
  348. return T;
  349. }
  350. PlatformStore::RESULT PlatformStore::buy(C Str &id, Bool subscription, C Str &data)
  351. {
  352. if(!id.is())return ITEM_UNAVAILABLE;
  353. if(!(subscription ? supportsSubscriptions() : supportsItems()))return SERVICE_UNAVAILABLE;
  354. #if WINDOWS_NEW
  355. create_task(WIN_STORE::RequestProductPurchaseAsync(ref new Platform::String(id))).then([id](task<Windows::ApplicationModel::Store::PurchaseResults^> task)
  356. {
  357. // this will be called on the main thread
  358. RESULT result=UNKNOWN;
  359. Purchase purchase;
  360. purchase.id=id;
  361. purchase.date.zero();
  362. try // exception may occur
  363. {
  364. if(auto results=task.get())
  365. {
  366. switch(results->Status)
  367. {
  368. case Windows::ApplicationModel::Store::ProductPurchaseStatus::Succeeded : result=PURCHASED ; break;
  369. case Windows::ApplicationModel::Store::ProductPurchaseStatus::AlreadyPurchased: result=ALREADY_OWNED; break;
  370. case Windows::ApplicationModel::Store::ProductPurchaseStatus::NotFulfilled : result=ALREADY_OWNED; break; // The transaction did not complete because the last purchase of this consumable in-app product has not been reported as fulfilled to the Windows Store - https://msdn.microsoft.com/en-us/library/windows/apps/windows.applicationmodel.store.productpurchasestatus
  371. case Windows::ApplicationModel::Store::ProductPurchaseStatus::NotPurchased : // The purchase did not occur because the user decided not to complete the transaction (or the transaction failed for other reasons) - https://msdn.microsoft.com/en-us/library/windows/apps/windows.applicationmodel.store.productpurchasestatus
  372. {
  373. if(Store._items.elms() && !Store.findItem(id))result=ITEM_UNAVAILABLE; // on Windows if we have information about one item, then it means we know all items, and if this item is not among them, then we know that it's unavailable
  374. else result=USER_CANCELED;
  375. }break;
  376. }
  377. if(result==PURCHASED || result==ALREADY_OWNED)
  378. {
  379. UID token; token.guid()=results->TransactionId; purchase.token=token.asHex();
  380. }
  381. if(results->ReceiptXml)
  382. {
  383. C wchar_t *receipt=results->ReceiptXml->Data();
  384. FileText f; f.readMem(receipt, Length(receipt)*SIZE(*receipt), UTF_16); // use 'UTF_16' because 'receipt' is a 16-bit string
  385. XmlData xml; xml.load(f);
  386. if(XmlNode *receipt=xml.findNode("Receipt"))
  387. if(XmlNode *ProductReceipt=receipt->findNode("ProductReceipt"))
  388. if(XmlParam *date=ProductReceipt->findParam("PurchaseDate"))
  389. ConvertDate(date->value, purchase.date);
  390. }
  391. // first add to the list of purchases
  392. if(result==PURCHASED && !Store.findPurchaseByToken(purchase.token))Store._purchases.add(purchase);
  393. }
  394. }
  395. catch(...){}
  396. // now call the callback
  397. if(Store.callback)Store.callback(result, &purchase); // !! here don't pass purchase from '_purchases' !!
  398. });
  399. return WAITING;
  400. #elif ANDROID
  401. JNI jni;
  402. if(jni && ActivityClass)
  403. if(JMethodID buy =jni->GetStaticMethodID(ActivityClass, "buy", "(Ljava/lang/String;Ljava/lang/String;Z)I"))
  404. if(JString j_id =JString(jni, id ))
  405. if(JString j_data=JString(jni, data))
  406. return (RESULT)jni->CallStaticIntMethod(ActivityClass, buy, j_id(), j_data(), jboolean(subscription));
  407. #elif IOS
  408. if(NSStringAuto ns_id=id)
  409. {
  410. SKMutablePayment *payment=[SKMutablePayment paymentWithProductIdentifier:ns_id];
  411. [[SKPaymentQueue defaultQueue] addPayment:payment];
  412. return WAITING;
  413. }
  414. #endif
  415. return SERVICE_UNAVAILABLE;
  416. }
  417. PlatformStore::RESULT PlatformStore::consume(C Str &token)
  418. {
  419. if(!token.is())return NOT_OWNED;
  420. #if WINDOWS_NEW
  421. C Purchase *purchase=findPurchaseByToken(token); if(!purchase)return NOT_OWNED;
  422. #if 0 // this is unavailable in the simulator, however since 'ReportConsumableFulfillmentAsync' works fine, then use just that
  423. WIN_STORE::ReportProductFulfillment(ref new Platform::String(purchase->id));
  424. #else
  425. UID token_id; token_id.fromHex(token);
  426. create_task(WIN_STORE::ReportConsumableFulfillmentAsync(ref new Platform::String(purchase->id), token_id.guid())).then([token](Windows::ApplicationModel::Store::FulfillmentResult result) // both 'ProductId' and 'TransactionId' must be specified
  427. {
  428. // this will be called on the main thread
  429. Purchase temp; C Purchase *existing=Store.findPurchaseByToken(token);
  430. if(existing)temp=*existing;else{temp.date.zero(); temp.token=token;} // copy to temp because we will remove it
  431. switch(result)
  432. {
  433. case Windows::ApplicationModel::Store::FulfillmentResult::PurchaseReverted:
  434. {
  435. Store._purchases.removeData(existing, true); // first remove from list of purchases
  436. if(Store.callback)Store.callback(REFUND, &temp); // now call the callback
  437. }break;
  438. case Windows::ApplicationModel::Store::FulfillmentResult::Succeeded:
  439. {
  440. Store._purchases.removeData(existing, true); // first remove from list of purchases
  441. if(Store.callback)Store.callback(CONSUMED, &temp); // now call the callback
  442. }break;
  443. }
  444. });
  445. #endif
  446. #elif ANDROID
  447. {
  448. SyncLocker locker(_lock);
  449. REPA(_consume)if(Equal(_consume[i], token, true))return WAITING;
  450. _consume.add(token);
  451. if(!_thread.active()){_thread.create(BackgroundUpdate, this); App._callbacks.include(Update, T);}
  452. }
  453. return WAITING;
  454. #elif IOS
  455. for(SKPaymentTransaction *transaction in [[SKPaymentQueue defaultQueue] transactions])
  456. if(Equal(AppleString(transaction.transactionIdentifier), token, true))
  457. {
  458. [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
  459. _purchases.removeData(findPurchaseByToken(token), true);
  460. return CONSUMED;
  461. }
  462. return NOT_OWNED;
  463. #endif
  464. return SERVICE_UNAVAILABLE;
  465. }
  466. /******************************************************************************/
  467. }
  468. /******************************************************************************/
  469. #if ANDROID
  470. extern "C"
  471. {
  472. /******************************************************************************/
  473. JNIEXPORT void JNICALL Java_com_esenthel_Native_connected(JNIEnv *env, jclass clazz, jboolean supports_items, jboolean supports_subs)
  474. {
  475. Store._supports_items=supports_items;
  476. Store._supports_subs =supports_subs;
  477. }
  478. JNIEXPORT jstring JNICALL Java_com_esenthel_Native_getStr(JNIEnv *env, jclass clazz, jlong user, jint i)
  479. {
  480. JNI jni(env);
  481. C MemPtr<Str> &strings=*(C MemPtr<Str>*)user;
  482. return jni->NewStringUTF(UTF8(InRange(i, strings) ? strings[i] : S));
  483. }
  484. JNIEXPORT void JNICALL Java_com_esenthel_Native_listItem(JNIEnv *env, jclass clazz, jlong user, jstring sku, jstring name, jstring desc, jstring price, jboolean sub)
  485. {
  486. JNI jni(env);
  487. MemPtr<PlatformStore::Item> &items=*(MemPtr<PlatformStore::Item>*)user;
  488. PlatformStore::Item &item =items.New();
  489. item.subscription=sub;
  490. item.id =jni(sku);
  491. item.name =jni(name);
  492. item.desc =jni(desc);
  493. item.price =jni(price);
  494. }
  495. JNIEXPORT void JNICALL Java_com_esenthel_Native_listPurchase(JNIEnv *env, jclass clazz, jlong user, jstring sku, jstring data, jstring token, jlong date)
  496. {
  497. JNI jni(env);
  498. MemPtr<PlatformStore::Purchase> &purchases=*(MemPtr<PlatformStore::Purchase>*)user;
  499. PlatformStore::Purchase &purchase =purchases.New();
  500. purchase.id =jni(sku);
  501. purchase.data =jni(data);
  502. purchase.token=jni(token);
  503. if(date)purchase.date.from1970ms(date);else purchase.date.zero();
  504. }
  505. JNIEXPORT void JNICALL Java_com_esenthel_Native_purchased(JNIEnv *env, jclass clazz, jint result, jstring sku, jstring data, jstring token, jlong date)
  506. {
  507. JNI jni(env);
  508. PlatformStore::Processed purchase;
  509. purchase.result=PlatformStore::RESULT(result);
  510. purchase.id =jni(sku);
  511. purchase.data =jni(data);
  512. purchase.token =jni(token);
  513. if(date)purchase.date.from1970ms(date);else purchase.date.zero();
  514. SyncLocker locker(Store._lock);
  515. Swap(Store._processed.New(), purchase);
  516. App._callbacks.include(Update, Store);
  517. }
  518. /******************************************************************************/
  519. }
  520. #endif
  521. /******************************************************************************/