Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
1
1weather
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Dmitriy Stepanets
1weather
Commits
0b88a0b4
Commit
0b88a0b4
authored
Sep 10, 2021
by
Demid Merzlyakov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
IOS-155: [WIP] purchase & verification methods.
parent
eaad2e8f
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
171 additions
and
21 deletions
+171
-21
1Weather/AppDelegate.swift
+11
-4
1Weather/InApps/StoreManager.swift
+159
-16
OneWeatherCore/OneWeatherCore/ModelObjects/Config/AppConfig.swift
+1
-1
No files found.
1Weather/AppDelegate.swift
View file @
0b88a0b4
...
@@ -47,7 +47,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
...
@@ -47,7 +47,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
ThemeManager
.
refreshAppearance
()
ThemeManager
.
refreshAppearance
()
UserDefaults
.
migrateUserDefaultsToAppGroupsIfNeeded
()
UserDefaults
.
migrateUserDefaultsToAppGroupsIfNeeded
()
storeManager
.
completeTransactions
()
storeManager
.
onStartup
()
if
let
launchOptions
=
launchOptions
{
if
let
launchOptions
=
launchOptions
{
log
.
debug
(
"Launch options:
\(
launchOptions
)
"
)
log
.
debug
(
"Launch options:
\(
launchOptions
)
"
)
...
@@ -205,12 +205,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
...
@@ -205,12 +205,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
return
storeManager
.
removeAdsPurchased
return
storeManager
.
removeAdsPurchased
}
}
let
p
roSubscription
=
ClosureFeatureAvailabilityChecker
{
[
weak
self
]
in
let
activeP
roSubscription
=
ClosureFeatureAvailabilityChecker
{
[
weak
self
]
in
guard
let
storeManager
=
self
?
.
storeManager
else
{
guard
let
storeManager
=
self
?
.
storeManager
else
{
return
false
return
false
}
}
return
storeManager
.
hasSubscription
return
storeManager
.
hasSubscription
}
}
let
everSubscribed
=
ClosureFeatureAvailabilityChecker
{
[
weak
self
]
in
guard
let
storeManager
=
self
?
.
storeManager
else
{
return
false
}
return
storeManager
.
everHadSubscription
}
let
isPhone
=
DeviceTypeFeatureAvailabilityChecker
(
deviceType
:
.
phone
)
let
isPhone
=
DeviceTypeFeatureAvailabilityChecker
(
deviceType
:
.
phone
)
var
availabilityCheckers
=
[
AppFeature
:
FeatureAvailabilityChecker
]()
var
availabilityCheckers
=
[
AppFeature
:
FeatureAvailabilityChecker
]()
...
@@ -219,9 +226,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
...
@@ -219,9 +226,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// To make sure all features are explicitly configured.
// To make sure all features are explicitly configured.
switch
feature
{
switch
feature
{
case
.
ads
:
case
.
ads
:
availabilityCheckers
[
feature
]
=
!
premium
&&
!
proSubscription
availabilityCheckers
[
feature
]
=
!
premium
&&
!
activeProSubscription
&&
!
everSubscribed
case
.
minutelyForecast
:
case
.
minutelyForecast
:
availabilityCheckers
[
feature
]
=
p
roSubscription
availabilityCheckers
[
feature
]
=
activeP
roSubscription
case
.
shorts
:
case
.
shorts
:
availabilityCheckers
[
feature
]
=
usOnly
&&
isPhone
availabilityCheckers
[
feature
]
=
usOnly
&&
isPhone
case
.
airQualityIndex
:
case
.
airQualityIndex
:
...
...
1Weather/InApps/StoreManager.swift
View file @
0b88a0b4
...
@@ -7,6 +7,7 @@
...
@@ -7,6 +7,7 @@
import
Foundation
import
Foundation
import
OneWeatherCore
import
OneWeatherCore
import
OneWeatherAnalytics
import
SwiftyStoreKit
import
SwiftyStoreKit
public
protocol
StoreManagerObserver
{
public
protocol
StoreManagerObserver
{
...
@@ -15,15 +16,24 @@ public protocol StoreManagerObserver {
...
@@ -15,15 +16,24 @@ public protocol StoreManagerObserver {
public
class
StoreManager
{
public
class
StoreManager
{
private
let
observers
=
MulticastDelegate
<
StoreManagerObserver
>
()
private
let
observers
=
MulticastDelegate
<
StoreManagerObserver
>
()
private
let
configManager
:
ConfigManager
private
let
log
=
Logger
(
componentName
:
"StoreManager 💸"
)
private
var
activeInAppRequests
=
[
String
:
InAppRequest
]()
private
static
let
sharedSecret
=
"9462f5b715774b2dafac935098e3f3c6"
public
static
let
shared
=
StoreManager
()
public
static
let
shared
=
StoreManager
()
public
init
(
configManager
:
ConfigManager
=
ConfigManager
.
shared
)
{
self
.
configManager
=
configManager
self
.
configManager
.
add
(
delegate
:
self
)
}
// MARK: - Get known status
// MARK: - Get known status
@UserDefaultsValue("com.inmobi.oneweather.WeatherKey", defaultValue: false, userDefaults: UserDefaults.appDefaults)
@UserDefaultsValue("com.inmobi.oneweather.WeatherKey", defaultValue: false, userDefaults: UserDefaults.appDefaults)
public
var
removeAdsPurchased
:
Bool
{
public
var
removeAdsPurchased
:
Bool
{
didSet
{
didSet
{
if
removeAdsPurchased
{
if
removeAdsPurchased
&&
removeAdsPurchased
!=
oldValue
{
observers
.
invoke
{
observer
in
observers
.
invoke
{
observer
in
observer
.
storeManagerUpdatedStatus
(
self
)
observer
.
storeManagerUpdatedStatus
(
self
)
}
}
...
@@ -31,31 +41,64 @@ public class StoreManager {
...
@@ -31,31 +41,64 @@ public class StoreManager {
}
}
}
}
@UserDefaultsValue("com.inmobi.oneweather.WeatherSubKey", defaultValue: false, userDefaults: UserDefaults.appDefaults)
public
var
hasSubscription
:
Bool
{
public
var
hasSubscription
:
Bool
{
#warning("Not implemented!")
didSet
{
//TODO: Implement!
if
hasSubscription
!=
oldValue
{
#if DEBUG
observers
.
invoke
{
observer
in
//TODO: REMOVE THIS'
observer
.
storeManagerUpdatedStatus
(
self
)
return
false
}
#warning("THIS IS A DEBUG-ONLY PIECE OF CODE! Not even temporary! Remove before making a production build.")
}
#else
if
hasSubscription
{
#error("THIS IS A DEBUG-ONLY PIECE OF CODE! Not even temporary! Remove before making a production build.")
self
.
everHadSubscription
=
true
#endif
}
}
}
@UserDefaultsValue("com.inmobi.oneweather.WeatherSubKeyEver", defaultValue: false, userDefaults: UserDefaults.appDefaults)
public
var
everHadSubscription
:
Bool
{
didSet
{
if
everHadSubscription
&&
everHadSubscription
!=
oldValue
{
observers
.
invoke
{
observer
in
observer
.
storeManagerUpdatedStatus
(
self
)
}
}
}
}
}
@UserDefaultsOptionalValue("com.inmobi.oneweather.WeatherSubKeyDate", userDefaults: UserDefaults.appDefaults)
public
var
subscriptionExpirationDate
:
Date
?
// MARK: - Actions
// MARK: - Actions
public
func
completeTransactions
()
{
public
func
onStartup
()
{
self
.
checkSubscriptionExpirationLocally
()
self
.
completeTransactions
()
self
.
updateProductInfo
()
#warning("Not implemented!")
//TODO: implement
// We need to verify subscription status on startup as well
}
private
func
completeTransactions
()
{
// This SHOULD ONLY BE CALLED ONCE, in the AppDelegate
// This SHOULD ONLY BE CALLED ONCE, in the AppDelegate
let
log
=
self
.
log
SwiftyStoreKit
.
completeTransactions
(
atomically
:
true
)
{
purchases
in
SwiftyStoreKit
.
completeTransactions
(
atomically
:
true
)
{
purchases
in
log
.
info
(
"Complete transactions for
\(
purchases
.
count
)
purchases"
)
for
purchase
in
purchases
{
for
purchase
in
purchases
{
log
.
info
(
"Purchase
\(
purchase
.
productId
)
:
\(
purchase
.
transaction
.
transactionState
)
"
)
switch
purchase
.
transaction
.
transactionState
{
switch
purchase
.
transaction
.
transactionState
{
case
.
purchased
,
.
restored
:
case
.
purchased
,
.
restored
:
if
purchase
.
needsFinishTransaction
{
if
purchase
.
needsFinishTransaction
{
// Deliver content from server, then:
SwiftyStoreKit
.
finishTransaction
(
purchase
.
transaction
)
SwiftyStoreKit
.
finishTransaction
(
purchase
.
transaction
)
}
}
// Unlock content
if
self
.
isPremiumSubscription
(
productId
:
purchase
.
productId
)
{
self
.
hasSubscription
=
true
self
.
everHadSubscription
=
true
}
else
if
purchase
.
productId
==
kInAppOneWeatherProId
{
self
.
removeAdsPurchased
=
true
}
case
.
failed
,
.
purchasing
,
.
deferred
:
case
.
failed
,
.
purchasing
,
.
deferred
:
break
// do nothing
break
// do nothing
@unknown
default
:
@unknown
default
:
...
@@ -65,14 +108,86 @@ public class StoreManager {
...
@@ -65,14 +108,86 @@ public class StoreManager {
}
}
}
}
public
func
purchase
(
subscription
:
SubscriptionConfig
)
{
public
func
updateProductInfo
()
{
let
log
=
self
.
log
log
.
debug
(
"Updating product info..."
)
let
config
=
configManager
.
config
.
subscriptionsConfig
guard
config
.
subscriptionsEnabled
else
{
log
.
debug
(
"Update product info: subscriptions disabled. Skip."
)
return
}
let
subscriptions
:
[
SubscriptionConfig
]
if
removeAdsPurchased
{
subscriptions
=
[
config
.
monthlyDiscounted
,
config
.
yearlyDiscounted
]
}
else
{
subscriptions
=
[
config
.
monthly
,
config
.
yearly
]
}
let
productIds
=
Set
<
String
>
(
subscriptions
.
map
{
$0
.
productId
})
SwiftyStoreKit
.
retrieveProductsInfo
(
productIds
)
{
result
in
if
let
error
=
result
.
error
{
log
.
error
(
"Update info: error:
\(
error
)
"
)
}
for
product
in
result
.
retrievedProducts
{
log
.
debug
(
"For
\(
product
.
productIdentifier
)
:
\(
product
.
debugDescription
)
"
)
}
}
}
}
public
func
updatePurchaseState
()
{
public
func
purchase
(
subscription
:
SubscriptionConfig
)
{
let
log
=
self
.
log
let
productId
=
subscription
.
productId
log
.
info
(
"Purchase
\(
productId
)
start."
)
let
request
:
InAppRequest
=
SwiftyStoreKit
.
purchaseProduct
(
productId
,
atomically
:
true
)
{
result
in
defer
{
self
.
activeInAppRequests
[
productId
]
=
nil
}
switch
result
{
case
.
success
(
let
product
):
if
product
.
needsFinishTransaction
{
SwiftyStoreKit
.
finishTransaction
(
product
.
transaction
)
}
case
.
error
(
error
:
let
error
):
log
.
error
(
"Purchase: error for
\(
productId
)
:
\(
error
)
"
)
}
}
activeInAppRequests
[
productId
]
=
request
}
}
public
func
verifySubscriptions
()
{
let
log
=
self
.
log
log
.
info
(
"Verify subscriptions..."
)
let
service
:
AppleReceiptValidator
.
VerifyReceiptURLType
#if DEBUG
service
=
.
production
#else
service
=
.
production
#endif
let
appleValidator
=
AppleReceiptValidator
(
service
:
service
,
sharedSecret
:
StoreManager
.
sharedSecret
)
SwiftyStoreKit
.
verifyReceipt
(
using
:
appleValidator
)
{
result
in
switch
result
{
case
.
success
(
let
receipt
):
let
purchaseResult
=
SwiftyStoreKit
.
verifySubscriptions
(
productIds
:
self
.
allSubscriptionIds
,
inReceipt
:
receipt
)
switch
purchaseResult
{
case
.
purchased
(
let
expiryDate
,
let
items
):
log
.
debug
(
"Verify: purchased subscription (valid until
\(
expiryDate
)
)! Items:
\(
items
)
"
)
self
.
hasSubscription
=
true
self
.
everHadSubscription
=
true
self
.
subscriptionExpirationDate
=
expiryDate
case
.
expired
(
let
expiryDate
,
let
items
):
log
.
debug
(
"Subscription expired since
\(
expiryDate
)\n\(
items
)\n
"
)
self
.
everHadSubscription
=
true
case
.
notPurchased
:
log
.
debug
(
"The user has never purchased a subscription."
)
self
.
hasSubscription
=
false
self
.
everHadSubscription
=
false
}
case
.
error
(
let
error
):
log
.
error
(
"Receipt verification failed:
\(
error
)
"
)
}
}
}
//MARK: - Observers management
//MARK: - Observers management
...
@@ -83,4 +198,32 @@ public class StoreManager {
...
@@ -83,4 +198,32 @@ public class StoreManager {
public
func
remove
(
observer
:
StoreManagerObserver
)
{
public
func
remove
(
observer
:
StoreManagerObserver
)
{
observers
.
remove
(
delegate
:
observer
)
observers
.
remove
(
delegate
:
observer
)
}
}
//MARK: - Private helper methods and vars
private
var
allSubscriptionIds
:
Set
<
String
>
{
let
config
=
configManager
.
config
.
subscriptionsConfig
let
subscriptionsList
=
[
config
.
monthly
,
config
.
yearly
,
config
.
monthlyDiscounted
,
config
.
yearlyDiscounted
]
.
map
{
$0
.
productId
}
return
Set
<
String
>
(
subscriptionsList
)
}
private
func
isPremiumSubscription
(
productId
:
String
)
->
Bool
{
return
allSubscriptionIds
.
contains
(
productId
)
}
private
func
checkSubscriptionExpirationLocally
()
{
guard
let
expirationDate
=
self
.
subscriptionExpirationDate
else
{
return
}
if
expirationDate
<
Date
()
{
self
.
hasSubscription
=
false
}
}
}
extension
StoreManager
:
ConfigManagerDelegate
{
public
func
dataUpdated
(
by
configManager
:
ConfigManager
)
{
self
.
updateProductInfo
()
}
}
}
OneWeatherCore/OneWeatherCore/ModelObjects/Config/AppConfig.swift
View file @
0b88a0b4
...
@@ -14,7 +14,7 @@ public struct AppConfig: Codable {
...
@@ -14,7 +14,7 @@ public struct AppConfig: Codable {
public
let
shortsLeftBelowCount
:
Int
public
let
shortsLeftBelowCount
:
Int
public
let
shortsSwipeUpNudgeCount
:
Int
public
let
shortsSwipeUpNudgeCount
:
Int
private
let
explicitFeatureAvailability
:
[
AppFeature
:
Bool
]
private
let
explicitFeatureAvailability
:
[
AppFeature
:
Bool
]
p
rivate
let
subscriptionsConfig
:
SubscriptionsListConfig
p
ublic
let
subscriptionsConfig
:
SubscriptionsListConfig
public
init
(
popularCities
:
[
GeoNamesPlace
]?,
public
init
(
popularCities
:
[
GeoNamesPlace
]?,
adConfig
:
AdConfig
,
adConfig
:
AdConfig
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment