[iOS] handle pending transactions and errors

This commit is contained in:
Aleksey Belouosv 2018-11-15 14:59:44 +03:00 committed by Olesia Bolovintseva
parent 8f329ca2ac
commit 8494933467
20 changed files with 280 additions and 59 deletions

View file

@ -19,12 +19,13 @@ struct CatalogCategoryInfo {
@objc(MWMCatalogWebViewController)
final class CatalogWebViewController: WebViewController {
let progressView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
let progressImageView = UIImageView(image: #imageLiteral(resourceName: "ic_24px_spinner"))
let numberOfTasksLabel = UILabel()
let loadingIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
let pendingTransactionsHandler = InAppPurchase.pendingTransactionsHandler()
var deeplink: URL?
var categoryInfo: CatalogCategoryInfo?
var statSent = false
var backButton: UIBarButtonItem!
var fwdButton: UIBarButtonItem!
@ -90,7 +91,6 @@ final class CatalogWebViewController: WebViewController {
progressView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8).isActive = true
}
rotateProgress()
updateProgress()
navigationItem.leftBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "ic_catalog_close"), style: .plain, target: self, action: #selector(goBack))
@ -110,6 +110,23 @@ final class CatalogWebViewController: WebViewController {
}
}
override func willLoadUrl(_ decisionHandler: @escaping (Bool) -> Void) {
pendingTransactionsHandler.handlePendingTransactions { [weak self] (status) in
switch status {
case .none:
fallthrough
case .success:
decisionHandler(true)
break
case .error:
MWMAlertViewController.activeAlert().presentInfoAlert(L("title_error_downloading_bookmarks"),
text: L("failed_purchase_support_message"))
decisionHandler(false)
self?.loadingIndicator.stopAnimating()
}
}
}
override func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
@ -157,7 +174,18 @@ final class CatalogWebViewController: WebViewController {
func processDeeplink(_ url: URL) {
guard let categoryInfo = parseUrl(url) else {
//TODO: handle error
MWMAlertViewController.activeAlert().presentInfoAlert(L("title_error_downloading_bookmarks"),
text: L("subtitle_error_downloading_guide"))
return
}
self.categoryInfo = categoryInfo
download()
}
private func download() {
guard let categoryInfo = self.categoryInfo else {
assert(false)
return
}
@ -187,7 +215,7 @@ final class CatalogWebViewController: WebViewController {
case .needAuth:
if let s = self {
s.signup(anchor: s.view) {
if $0 { s.processDeeplink(url) }
if $0 { s.download() }
}
}
break
@ -195,7 +223,7 @@ final class CatalogWebViewController: WebViewController {
self?.showPaymentScreen(categoryInfo)
break
case .notFound:
self?.showServerError(url)
self?.showServerError()
break
case .networkError:
self?.showNetworkError()
@ -218,7 +246,8 @@ final class CatalogWebViewController: WebViewController {
private func showPaymentScreen(_ productInfo: CatalogCategoryInfo) {
guard let productId = productInfo.productId else {
//TODO: handle error
MWMAlertViewController.activeAlert().presentInfoAlert(L("title_error_downloading_bookmarks"),
text: L("subtitle_error_downloading_guide"))
return
}
@ -242,12 +271,12 @@ final class CatalogWebViewController: WebViewController {
MWMAlertViewController.activeAlert().presentNoConnectionAlert();
}
private func showServerError(_ url: URL) {
private func showServerError() {
MWMAlertViewController.activeAlert().presentDefaultAlert(withTitle: L("error_server_title"),
message: L("error_server_message"),
rightButtonTitle: L("try_again"),
leftButtonTitle: L("cancel")) {
self.processDeeplink(url)
self.download()
}
}
@ -284,6 +313,7 @@ final class CatalogWebViewController: WebViewController {
extension CatalogWebViewController: PaidRouteViewControllerDelegate {
func didCompletePurchase(_ viewController: PaidRouteViewController) {
dismiss(animated: true)
download()
}
func didCancelPurchase(_ viewController: PaidRouteViewController) {

View file

@ -40,7 +40,14 @@ class PaidRouteViewController: MWMViewController {
routeTitleLabel.text = name
routeAuthorLabel.text = author
if let url = imageUrl {
previewImageView.af_setImage(withURL: url)
previewImageView.af_setImage(withURL: url,
placeholderImage: nil,
filter: nil,
progress: nil,
progressQueue: DispatchQueue.main,
imageTransition: .crossDissolve(0.25),
runImageTransitionIfCached: true,
completion: nil)
}
purchase.requestStoreProduct { [weak self] (product, error) in
@ -52,11 +59,16 @@ class PaidRouteViewController: MWMViewController {
return
}
self?.productNameLabel.text = product.localizedName
self?.buyButton.setTitle(product.formattedPrice, for: .normal)
self?.buyButton.setTitle(String(coreFormat: L("buy_btn"), arguments: [product.formattedPrice]),
for: .normal)
self?.buyButton.isEnabled = true
}
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
// MARK: - Event handlers
@IBAction func onBuy(_ sender: UIButton) {

View file

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="landscape">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -16,7 +16,7 @@
<outlet property="cancelButton" destination="Y4X-z8-enR" id="mDn-Yz-e77"/>
<outlet property="loadingIndicator" destination="Yat-hc-p6d" id="8aX-Nj-New"/>
<outlet property="loadingView" destination="I10-rL-z9q" id="27n-ZW-Q7a"/>
<outlet property="previewImageView" destination="1jX-9f-swC" id="WDk-rk-9D6"/>
<outlet property="previewImageView" destination="wJO-dJ-F8G" id="s2t-aS-INP"/>
<outlet property="productNameLabel" destination="Pwv-EO-6fY" id="U5q-xr-CyD"/>
<outlet property="routeAuthorLabel" destination="H6m-mO-TFt" id="GE3-sO-eOC"/>
<outlet property="routeTitleLabel" destination="4PP-cK-o7Y" id="Nez-Ie-dZl"/>
@ -25,21 +25,24 @@
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view clipsSubviews="YES" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="667" height="375"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" verticalCompressionResistancePriority="749" image="img_guides_placeholder" translatesAutoresizingMaskIntoConstraints="NO" id="1jX-9f-swC">
<rect key="frame" x="0.0" y="-265.5" width="375" height="568.5"/>
<rect key="frame" x="0.0" y="-151" width="347" height="526"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" secondItem="1jX-9f-swC" secondAttribute="height" multiplier="500:758" id="ZSu-8v-nw1"/>
</constraints>
</imageView>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="wJO-dJ-F8G">
<rect key="frame" x="0.0" y="0.0" width="347" height="375"/>
</imageView>
<view contentMode="scaleToFill" horizontalCompressionResistancePriority="751" verticalCompressionResistancePriority="751" translatesAutoresizingMaskIntoConstraints="NO" id="pbL-bL-mXm">
<rect key="frame" x="0.0" y="303" width="375" height="364"/>
<rect key="frame" x="347" y="0.0" width="320" height="375"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="752" text="..." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Pwv-EO-6fY">
<rect key="frame" x="24" y="24" width="327" height="17"/>
<rect key="frame" x="24" y="24" width="272" height="17"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="14"/>
<color key="textColor" white="0.0" alpha="0.37553510273972601" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@ -49,7 +52,7 @@
</userDefinedRuntimeAttributes>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="YlO-Qt-ILN">
<rect key="frame" x="0.0" y="57" width="375" height="1"/>
<rect key="frame" x="0.0" y="57" width="320" height="1"/>
<color key="backgroundColor" white="0.0" alpha="0.1210134845890411" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="LSa-3h-jdQ"/>
@ -59,7 +62,7 @@
</userDefinedRuntimeAttributes>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="751" text="Have a dinner with Hemingway and Castro in Havana" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4PP-cK-o7Y">
<rect key="frame" x="24" y="74" width="327" height="53"/>
<rect key="frame" x="24" y="74" width="272" height="79"/>
<fontDescription key="fontDescription" type="system" pointSize="22"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -70,7 +73,7 @@
</userDefinedRuntimeAttributes>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="752" text="by Julio Mulio" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="H6m-mO-TFt">
<rect key="frame" x="24" y="143" width="327" height="17"/>
<rect key="frame" x="24" y="169" width="272" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="0.0" alpha="0.87625749143835618" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@ -81,7 +84,7 @@
</userDefinedRuntimeAttributes>
</label>
<button opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="751" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1XW-gh-X68" userLabel="Download Bookmarks">
<rect key="frame" x="24" y="184" width="327" height="48"/>
<rect key="frame" x="24" y="249" width="272" height="48"/>
<color key="backgroundColor" red="0.11764705882352941" green="0.58823529411764708" blue="0.94117647058823528" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="48" id="d2Y-HN-jxk"/>
@ -106,7 +109,7 @@
</connections>
</button>
<button opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="751" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Y4X-z8-enR" userLabel="Cancel">
<rect key="frame" x="24" y="242" width="327" height="48"/>
<rect key="frame" x="24" y="307" width="272" height="48"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="48" id="bWF-O1-9Gw"/>
@ -126,7 +129,7 @@
</connections>
</button>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" animating="YES" style="white" translatesAutoresizingMaskIntoConstraints="NO" id="Yat-hc-p6d">
<rect key="frame" x="177.5" y="198" width="20" height="20"/>
<rect key="frame" x="150" y="263" width="20" height="20"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@ -200,10 +203,10 @@
</variation>
</view>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="I10-rL-z9q">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="667" height="375"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" animating="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="m8U-2p-bdf">
<rect key="frame" x="169" y="315" width="37" height="37"/>
<rect key="frame" x="315" y="169" width="37" height="37"/>
<color key="color" white="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</activityIndicatorView>
</subviews>
@ -221,17 +224,21 @@
<constraints>
<constraint firstItem="1jX-9f-swC" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="0as-MJ-IuO"/>
<constraint firstItem="I10-rL-z9q" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="1ok-WA-uNG"/>
<constraint firstItem="wJO-dJ-F8G" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="3sj-tj-YDX"/>
<constraint firstItem="I10-rL-z9q" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" id="Gtb-Sq-SC0"/>
<constraint firstItem="fnl-2z-Ty3" firstAttribute="bottom" secondItem="pbL-bL-mXm" secondAttribute="bottom" id="Lrf-Mc-vHX"/>
<constraint firstAttribute="bottom" secondItem="1jX-9f-swC" secondAttribute="bottom" id="M6z-RM-5RE"/>
<constraint firstItem="pbL-bL-mXm" firstAttribute="leading" secondItem="1jX-9f-swC" secondAttribute="trailing" id="Xht-Dc-57g"/>
<constraint firstItem="wJO-dJ-F8G" firstAttribute="bottom" secondItem="1jX-9f-swC" secondAttribute="bottom" id="Yzc-lV-S0m"/>
<constraint firstAttribute="trailing" secondItem="1jX-9f-swC" secondAttribute="trailing" id="ZKR-uE-TDM"/>
<constraint firstAttribute="bottom" secondItem="I10-rL-z9q" secondAttribute="bottom" id="b85-Qx-MiP"/>
<constraint firstItem="wJO-dJ-F8G" firstAttribute="trailing" secondItem="1jX-9f-swC" secondAttribute="trailing" id="cgA-qO-qjx"/>
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="pbL-bL-mXm" secondAttribute="trailing" id="ezH-PS-MTK"/>
<constraint firstItem="1jX-9f-swC" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" id="gJB-J9-Wvt"/>
<constraint firstItem="pbL-bL-mXm" firstAttribute="top" secondItem="1jX-9f-swC" secondAttribute="bottom" id="mbr-U9-QqH"/>
<constraint firstItem="pbL-bL-mXm" firstAttribute="top" relation="greaterThanOrEqual" secondItem="fnl-2z-Ty3" secondAttribute="top" id="mcX-zM-axH"/>
<constraint firstItem="pbL-bL-mXm" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="pel-8r-WlL"/>
<constraint firstItem="wJO-dJ-F8G" firstAttribute="leading" secondItem="1jX-9f-swC" secondAttribute="leading" id="pv1-4b-pAr"/>
<constraint firstAttribute="trailing" secondItem="I10-rL-z9q" secondAttribute="trailing" id="qDe-SK-EBd"/>
<constraint firstItem="pbL-bL-mXm" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="skf-ew-o90"/>
</constraints>

View file

@ -130,6 +130,7 @@ using namespace osm_auth_ios;
@property(nonatomic) NSInteger standbyCounter;
@property(nonatomic) MWMBackgroundFetchScheduler * backgroundFetchScheduler;
@property(nonatomic) id<IPendingTransactionsHandler> pendingTransactionHandler;
@end
@ -390,8 +391,14 @@ using namespace osm_auth_ios;
if (@available(iOS 10, *))
[UNUserNotificationCenter currentNotificationCenter].delegate = self;
if ([MWMFrameworkHelper canUseNetwork])
if ([MWMFrameworkHelper canUseNetwork]) {
[[SubscriptionManager shared] validate];
self.pendingTransactionHandler = [InAppPurchase pendingTransactionsHandler];
__weak __typeof(self) ws = self;
[self.pendingTransactionHandler handlePendingTransactions:^(PendingTransactionsStatus) {
ws.pendingTransactionHandler = nil;
}];
}
return YES;
}

View file

@ -2,6 +2,7 @@ typedef void (^MWMVoidBlock)(void);
typedef void (^MWMStringBlock)(NSString *);
typedef void (^MWMURLBlock)(NSURL *);
typedef BOOL (^MWMCheckStringBlock)(NSString *);
typedef void (^MWMBoolBlock)(BOOL);
typedef NS_ENUM(NSUInteger, MWMDayTime) { MWMDayTimeDay, MWMDayTimeNight };

View file

@ -2,6 +2,8 @@
#import "MWMViewController.h"
#import "MWMTypes.h"
NS_ASSUME_NONNULL_BEGIN
@interface WebViewController : MWMViewController <WKNavigationDelegate>
@property (nonatomic) NSURL * _Nullable m_url;
@ -9,14 +11,17 @@
// Set to YES if external browser should be launched
@property (nonatomic) BOOL openInSafari;
- (instancetype _Nullable)initWithUrl:(NSURL * _Nonnull)url title:( NSString * _Nullable)title;
- (instancetype _Nullable)initWithHtml:(NSString * _Nonnull)htmlText
- (instancetype _Nullable)initWithUrl:(NSURL *)url title:( NSString * _Nullable)title;
- (instancetype _Nullable)initWithHtml:(NSString *)htmlText
baseUrl:(NSURL * _Nullable)url
title:(NSString * _Nullable)title;
- (instancetype _Nullable)initWithAuthURL:(NSURL * _Nonnull)url
- (instancetype _Nullable)initWithAuthURL:(NSURL *)url
onSuccessAuth:(MWMStringBlock _Nullable)success
onFailure:(MWMVoidBlock _Nullable)failure;
- (void)willLoadUrl:(MWMBoolBlock)decisionHandler;
- (void)forward;
- (void)back;
@end
NS_ASSUME_NONNULL_END

View file

@ -87,16 +87,26 @@
self.webView.backgroundColor = UIColor.whiteColor;
if (self.m_htmlText)
{
[self.webView loadHTMLString:self.m_htmlText baseURL:self.m_url];
}
else
{
auto request = [NSMutableURLRequest requestWithURL:self.m_url];
[request setValue:@(GetPlatform().GetAppUserAgent().Get().c_str()) forHTTPHeaderField:@"User-Agent"];
[self.webView loadRequest:request];
}
__weak __typeof(self) ws = self;
[self willLoadUrl:^(BOOL load) {
__typeof(self) self = ws;
if (load) {
if (self.m_htmlText)
{
[self.webView loadHTMLString:self.m_htmlText baseURL:self.m_url];
}
else
{
auto request = [NSMutableURLRequest requestWithURL:self.m_url];
[request setValue:@(GetPlatform().GetAppUserAgent().Get().c_str()) forHTTPHeaderField:@"User-Agent"];
[self.webView loadRequest:request];
}
}
}];
}
- (void)willLoadUrl:(MWMBoolBlock)decisionHandler {
decisionHandler(YES);
}
- (void)viewDidDisappear:(BOOL)animated

View file

@ -0,0 +1,11 @@
enum TransactionStatus {
case none
case paid
case failed
case error
}
protocol IBillingPendingTransaction {
var status: TransactionStatus { get }
func finishTransaction()
}

View file

@ -16,7 +16,7 @@ protocol IInAppBilling {
typealias ProductsCompletion = ([IBillingProduct]?, Error?) -> Void
typealias PaymentCompletion = (PaymentStatus, Error?) -> Void
func requestProduct(_ productIds: Set<String>, completion: @escaping ProductsCompletion)
func requestProducts(_ productIds: Set<String>, completion: @escaping ProductsCompletion)
func makePayment(_ product: IBillingProduct, completion: @escaping PaymentCompletion)
func finishTransaction()
}

View file

@ -0,0 +1,13 @@
@objc
enum PendingTransactionsStatus: Int {
case none
case success
case error
}
@objc
protocol IPendingTransactionsHandler {
typealias PendingTransactinsCompletion = (PendingTransactionsStatus) -> Void
func handlePendingTransactions(_ completion: @escaping PendingTransactinsCompletion)
}

View file

@ -0,0 +1,54 @@
final class BillingPendingTransaction: NSObject, IBillingPendingTransaction {
private var pendingTransaction: SKPaymentTransaction?
override init() {
super.init()
SKPaymentQueue.default().add(self)
}
deinit {
SKPaymentQueue.default().remove(self)
}
var status: TransactionStatus {
let routeTransactions = SKPaymentQueue.default().transactions.filter {
!Subscription.productIds.contains($0.payment.productIdentifier)
}
if routeTransactions.count > 1 {
return .error
} else if routeTransactions.count == 1 {
pendingTransaction = routeTransactions[0]
switch pendingTransaction!.transactionState {
case .purchasing:
fallthrough
case .failed:
return .failed
case .purchased:
fallthrough
case .restored:
fallthrough
case .deferred:
return .paid
}
} else {
return .none
}
}
func finishTransaction() {
guard let transaction = pendingTransaction else {
assert(false, "There is no pending transactions")
return
}
SKPaymentQueue.default().finishTransaction(transaction)
pendingTransaction = nil
}
}
extension BillingPendingTransaction: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
// Do nothing. Only for SKPaymentQueue.default().transactions to work
}
}

View file

@ -38,7 +38,7 @@ final class InAppBilling: NSObject, IInAppBilling {
SKPaymentQueue.default().remove(self)
}
func requestProduct(_ productIds: Set<String>, completion: @escaping ProductsCompletion) {
func requestProducts(_ productIds: Set<String>, completion: @escaping ProductsCompletion) {
productsCompletion = completion
productRequest = SKProductsRequest(productIdentifiers: productIds)
productRequest!.delegate = self

View file

@ -4,7 +4,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface MWMPurchaseValidation : NSObject <IMWMPurchaseValidation>
- (instancetype)init __unavailable;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithVendorId:(NSString *)vendorId;
@end

View file

@ -33,12 +33,12 @@ final class PaidRoutePurchase: NSObject, IPaidRoutePurchase {
}
func requestStoreProduct(_ completion: @escaping StoreProductCompletion) {
billing.requestProduct([productId]) { [unowned self] (products, error) in
billing.requestProducts([productId]) { [weak self] (products, error) in
guard let product = products?[0] else {
completion(nil, error)
return
}
self.billingProduct = product
self?.billingProduct = product
completion(StoreProduct(product), nil)
}
}
@ -46,6 +46,7 @@ final class PaidRoutePurchase: NSObject, IPaidRoutePurchase {
func makePayment(_ completion: @escaping StorePaymentCompletion) {
guard let product = billingProduct else {
assert(false, "You must call requestStoreProduct() first")
return
}
storePaymentCompletion = completion
@ -71,19 +72,19 @@ final class PaidRoutePurchase: NSObject, IPaidRoutePurchase {
}
private func purchased() {
purchaseValidation.validateReceipt(serverId, callback: { [unowned self] result in
purchaseValidation.validateReceipt(serverId, callback: { [weak self] result in
switch result {
case .valid:
self.billing.finishTransaction()
self.storePaymentCompletion?(.success, nil)
self?.billing.finishTransaction()
self?.storePaymentCompletion?(.success, nil)
break
case .notValid:
fallthrough
case .error:
self.storePaymentCompletion?(.error, nil)
self?.storePaymentCompletion?(.error, nil)
break
}
self.storePaymentCompletion = nil
self?.storePaymentCompletion = nil
})
}

View file

@ -0,0 +1,36 @@
final class PendingTransactionsHandler: IPendingTransactionsHandler {
private let purchaseValidation: IMWMPurchaseValidation
private let pendingTransaction: IBillingPendingTransaction
init(validation: IMWMPurchaseValidation, pendingTransaction: IBillingPendingTransaction) {
self.purchaseValidation = validation
self.pendingTransaction = pendingTransaction
}
func handlePendingTransactions(_ completion: @escaping PendingTransactinsCompletion) {
switch pendingTransaction.status {
case .none:
completion(.none)
break
case .paid:
purchaseValidation.validateReceipt("") { [weak self] in
switch $0 {
case .valid:
completion(.success)
self?.pendingTransaction.finishTransaction()
case .notValid:
fallthrough
case .error:
completion(.error)
}
}
break
case .failed:
pendingTransaction.finishTransaction()
break
case .error:
completion(.error)
break
}
}
}

View file

@ -1,4 +1,5 @@
final class InAppPurchase {
@objc
final class InAppPurchase: NSObject {
static func paidRoutePurchase(serverId: String,
productId: String) -> IPaidRoutePurchase {
let validation = MWMPurchaseValidation(vendorId: BOOKMARKS_VENDOR)
@ -8,4 +9,11 @@ final class InAppPurchase {
purchaseValidation: validation,
billing: billing)
}
@objc
static func pendingTransactionsHandler() -> IPendingTransactionsHandler {
let validation = MWMPurchaseValidation(vendorId: BOOKMARKS_VENDOR)
let pendingTransaction = BillingPendingTransaction()
return PendingTransactionsHandler(validation: validation, pendingTransaction: pendingTransaction)
}
}

View file

@ -83,7 +83,8 @@ class SubscriptionManager: NSObject {
}
MWMPurchaseManager.shared().setAdsDisabled(validationResult == .valid)
self.paymentQueue.transactions
.filter { $0.transactionState == .purchased || $0.transactionState == .restored }
.filter { Subscription.productIds.contains($0.payment.productIdentifier) &&
($0.transactionState == .purchased || $0.transactionState == .restored) }
.forEach { self.paymentQueue.finishTransaction($0) }
self.restorationCallback?(validationResult)
self.restorationCallback = nil

View file

@ -357,6 +357,9 @@
470F5A5C2181DE7500754295 /* PaidRouteViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 470F5A5A2181DE7400754295 /* PaidRouteViewController.xib */; };
470F5A7D2189BB2F00754295 /* PaidRoutePurchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 470F5A7C2189BB2F00754295 /* PaidRoutePurchase.swift */; };
470F5A7F2189C30800754295 /* InAppPurchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 470F5A7E2189C30800754295 /* InAppPurchase.swift */; };
4719A643219CB61D009F9AA7 /* BillingPendingTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4719A642219CB61D009F9AA7 /* BillingPendingTransaction.swift */; };
4719A645219CBD65009F9AA7 /* IPendingTransactionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4719A644219CBD65009F9AA7 /* IPendingTransactionsHandler.swift */; };
4719A647219CBD7F009F9AA7 /* IBillingPendingTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4719A646219CBD7F009F9AA7 /* IBillingPendingTransaction.swift */; };
471BBD942130390F00EB17C9 /* TutorialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471BBD932130390F00EB17C9 /* TutorialViewController.swift */; };
472E3F472146BCD30020E412 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472E3F462146BCD30020E412 /* SubscriptionManager.swift */; };
472E3F4A2146C4CD0020E412 /* MWMPurchaseManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 472E3F492146C4CD0020E412 /* MWMPurchaseManager.mm */; };
@ -382,6 +385,7 @@
47B505552136B0CF009CBB55 /* SearchTutorialBlur.xib in Resources */ = {isa = PBXBuildFile; fileRef = 47B505532136AD69009CBB55 /* SearchTutorialBlur.xib */; };
47C7F9732191E15A00C2760C /* InAppBilling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C7F9722191E15A00C2760C /* InAppBilling.swift */; };
47C7F97521930F5300C2760C /* IInAppBilling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C7F97421930F5300C2760C /* IInAppBilling.swift */; };
47D0026721999DA900F651A2 /* PendingTransactionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47D0026621999DA900F651A2 /* PendingTransactionsHandler.swift */; };
47E3C72121108E9F008B3B27 /* BookmarksLoadedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47E3C71F21108E9F008B3B27 /* BookmarksLoadedViewController.swift */; };
47E3C72221108E9F008B3B27 /* BookmarksLoadedViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 47E3C72021108E9F008B3B27 /* BookmarksLoadedViewController.xib */; };
47E3C7252111E41B008B3B27 /* DimmedModalPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47E3C7242111E41B008B3B27 /* DimmedModalPresentationController.swift */; };
@ -1354,6 +1358,9 @@
470F5A5A2181DE7400754295 /* PaidRouteViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PaidRouteViewController.xib; sourceTree = "<group>"; };
470F5A7C2189BB2F00754295 /* PaidRoutePurchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaidRoutePurchase.swift; sourceTree = "<group>"; };
470F5A7E2189C30800754295 /* InAppPurchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchase.swift; sourceTree = "<group>"; };
4719A642219CB61D009F9AA7 /* BillingPendingTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BillingPendingTransaction.swift; sourceTree = "<group>"; };
4719A644219CBD65009F9AA7 /* IPendingTransactionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPendingTransactionsHandler.swift; sourceTree = "<group>"; };
4719A646219CBD7F009F9AA7 /* IBillingPendingTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IBillingPendingTransaction.swift; sourceTree = "<group>"; };
471BBD932130390F00EB17C9 /* TutorialViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TutorialViewController.swift; sourceTree = "<group>"; };
472E3F462146BCD30020E412 /* SubscriptionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = "<group>"; };
472E3F482146C4CD0020E412 /* MWMPurchaseManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MWMPurchaseManager.h; sourceTree = "<group>"; };
@ -1384,6 +1391,7 @@
47C7F9722191E15A00C2760C /* InAppBilling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppBilling.swift; sourceTree = "<group>"; };
47C7F97421930F5300C2760C /* IInAppBilling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IInAppBilling.swift; sourceTree = "<group>"; };
47C7F976219310D800C2760C /* IMWMPurchaseValidation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IMWMPurchaseValidation.h; sourceTree = "<group>"; };
47D0026621999DA900F651A2 /* PendingTransactionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingTransactionsHandler.swift; sourceTree = "<group>"; };
47E3C71F21108E9F008B3B27 /* BookmarksLoadedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksLoadedViewController.swift; sourceTree = "<group>"; };
47E3C72021108E9F008B3B27 /* BookmarksLoadedViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BookmarksLoadedViewController.xib; sourceTree = "<group>"; };
47E3C7242111E41B008B3B27 /* DimmedModalPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DimmedModalPresentationController.swift; sourceTree = "<group>"; };
@ -3236,18 +3244,30 @@
470F5A7B2189BA7200754295 /* InappPurchase */ = {
isa = PBXGroup;
children = (
477D7AC6218F1515007EE2CB /* IPaidRoutePurchase.swift */,
470F5A7C2189BB2F00754295 /* PaidRoutePurchase.swift */,
470F5A7E2189C30800754295 /* InAppPurchase.swift */,
47C7F976219310D800C2760C /* IMWMPurchaseValidation.h */,
473464A5218B0BC000D6AF5B /* MWMPurchaseValidation.h */,
473464A6218B0BC000D6AF5B /* MWMPurchaseValidation.mm */,
4719A648219CBD9E009F9AA7 /* Impl */,
4719A646219CBD7F009F9AA7 /* IBillingPendingTransaction.swift */,
47C7F97421930F5300C2760C /* IInAppBilling.swift */,
47C7F9722191E15A00C2760C /* InAppBilling.swift */,
477D7AC6218F1515007EE2CB /* IPaidRoutePurchase.swift */,
4719A644219CBD65009F9AA7 /* IPendingTransactionsHandler.swift */,
47C7F976219310D800C2760C /* IMWMPurchaseValidation.h */,
);
path = InappPurchase;
sourceTree = "<group>";
};
4719A648219CBD9E009F9AA7 /* Impl */ = {
isa = PBXGroup;
children = (
4719A642219CB61D009F9AA7 /* BillingPendingTransaction.swift */,
47C7F9722191E15A00C2760C /* InAppBilling.swift */,
470F5A7C2189BB2F00754295 /* PaidRoutePurchase.swift */,
47D0026621999DA900F651A2 /* PendingTransactionsHandler.swift */,
473464A5218B0BC000D6AF5B /* MWMPurchaseValidation.h */,
473464A6218B0BC000D6AF5B /* MWMPurchaseValidation.mm */,
);
path = Impl;
sourceTree = "<group>";
};
471BBD92213038E000EB17C9 /* TipsAndTricks */ = {
isa = PBXGroup;
children = (
@ -4768,6 +4788,7 @@
34BBD64C1F826DB10070CA50 /* AuthorizationViewController.swift in Sources */,
F6E2FE101E097BA00083EBEC /* MWMOpeningHoursTableViewCell.mm in Sources */,
6741A9B01BF340DE002C974C /* MapsAppDelegate.mm in Sources */,
4719A645219CBD65009F9AA7 /* IPendingTransactionsHandler.swift in Sources */,
34F742321E0834F400AC1FD6 /* UIViewController+Navigation.mm in Sources */,
340475811E081B3300C92850 /* iosOGLContextFactory.mm in Sources */,
34AB66561FC5AA330078E451 /* TransportTransitPedestrian.swift in Sources */,
@ -4865,6 +4886,7 @@
34D3B0181E389D05004100F9 /* EditorAdditionalNamePlaceholderTableViewCell.swift in Sources */,
346DB8281E5C4F6700E3123E /* GalleryCell.swift in Sources */,
F6EBB26F1FD7E33300B69B6A /* DiscoveryNoResultsCell.swift in Sources */,
47D0026721999DA900F651A2 /* PendingTransactionsHandler.swift in Sources */,
F6E2FF121E097BA00083EBEC /* MWMSearchHistoryRequestCell.mm in Sources */,
F6FE3C391CC50FFD00A73196 /* MWMPlaceDoesntExistAlert.mm in Sources */,
F6E2FDFE1E097BA00083EBEC /* MWMOpeningHoursClosedSpanTableViewCell.mm in Sources */,
@ -4948,6 +4970,7 @@
3472B5E1200F86C800DC6CD5 /* MWMEditorHelper.mm in Sources */,
F6E2FE3D1E097BA00083EBEC /* MWMMigrationViewController.mm in Sources */,
F6E2FD501E097BA00083EBEC /* MWMMapDownloaderAdsTableViewCell.mm in Sources */,
4719A643219CB61D009F9AA7 /* BillingPendingTransaction.swift in Sources */,
F6E2FE881E097BA00083EBEC /* MWMPlacePageRegularCell.mm in Sources */,
34AB66771FC5AA330078E451 /* TransportRoutePreviewStatus.swift in Sources */,
F6E2FD801E097BA00083EBEC /* MWMMapDownloaderExtendedDataSourceWithAds.mm in Sources */,
@ -4998,6 +5021,7 @@
F682249B1E5B104600BC1C18 /* PPHotelDescriptionCell.swift in Sources */,
F6E2FECA1E097BA00083EBEC /* MWMSearchFilterTransitioning.mm in Sources */,
3454D7DD1E07F045004AF2AD /* UISwitch+RuntimeAttributes.m in Sources */,
4719A647219CBD7F009F9AA7 /* IBillingPendingTransaction.swift in Sources */,
340416581E7C0D4100E2B6D6 /* PhotosOverlayView.swift in Sources */,
F6E2FE821E097BA00083EBEC /* MWMPlacePageOpeningHoursDayView.mm in Sources */,
340FDC092031C39E00F140AD /* BMCPermissionsPendingCell.swift in Sources */,

View file

@ -42,7 +42,8 @@ public:
std::string m_vendorId;
std::string m_receiptData;
bool IsValid() const { return !m_serverId.empty() && !m_vendorId.empty() && !m_receiptData.empty(); }
// "We do not check serverId here, because it can be empty in some cases."
bool IsValid() const { return !m_vendorId.empty() && !m_receiptData.empty(); }
};
using ValidationCallback = std::function<void(ValidationCode, ValidationInfo const &)>;