From 35621077e43fcd5900ffee3ce6134fc1f11d74ea Mon Sep 17 00:00:00 2001 From: Steven Bankhead Date: Thu, 18 Mar 2021 18:39:56 -0700 Subject: [PATCH] Release 3.6.2 --- UnityAds.podspec | 4 +- UnityAds/PrefixHeader.pch | 1 + .../Categories/NSDate/NSDate_NSNumberTests.m | 20 ++ .../NSMutableDictionary_SafeOperationsTests.m | 28 +++ .../Mocks/NSInvocationTargetMock.h | 37 ++++ .../Mocks/NSInvocationTargetMock.m | 51 +++++ .../NSInvocation/NSInvocationTests.m | 114 +++++++++++ .../Mocks/UADSBridgeMock.h | 16 ++ .../Mocks/UADSBridgeMock.m | 64 +++++++ .../Mocks/UADSProxyReflectionMock.h | 9 + .../Mocks/UADSProxyReflectionMock.m | 8 + .../UADSProxyReflectionTests.m | 115 ++++++++++++ UnityAdsTests/UnityAdsTests-Bridging-Header.h | 1 + .../Mocks/NSLocale + CustomInit.h | 10 + .../Mocks/NSLocale + CustomInit.m | 16 ++ .../Mocks/SKProduct + CustomInit.h | 10 + .../Mocks/SKProduct + CustomInit.m | 73 ++++++++ .../Mocks/SKProductDiscount + CustomInit.h | 10 + .../Mocks/SKProductDiscount + CustomInit.m | 55 ++++++ ...SKProductSubscriptionPeriod + CustomInit.h | 11 ++ ...SKProductSubscriptionPeriod + CustomInit.m | 22 +++ .../StoreKitBridges/SKProductBridgeTests.m | 145 ++++++++++++++ .../Core/Properties/USRVSdkProperties.m | 4 +- UnityServices/Store/Api/USTRApiProducts.m | 50 ++++- .../NSLocale + PriceDictionary.h | 10 + .../NSLocale + PriceDictionary.m | 20 ++ .../SKPayment + Dictionary.h | 9 + .../SKPayment + Dictionary.m | 25 +++ .../SKPaymentTransaction + Dictionary.h | 9 + .../SKPaymentTransaction + Dictionary.m | 35 ++++ .../SKProductBridge + Dictionary.h | 8 + .../SKProductBridge + Dictionary.m | 68 +++++++ .../SKProductDiscountBridge + Dictionary.h | 9 + .../SKProductDiscountBridge + Dictionary.m | 34 ++++ ...uctSubscriptionPeriodBridge + Dictionary.h | 9 + ...uctSubscriptionPeriodBridge + Dictionary.m | 15 ++ .../SKProductsRequest+UniqueID.h | 9 + .../SKProductsRequest+UniqueID.m | 8 + .../SKProductBridge/SKProductBridge.h | 33 ++++ .../SKProductBridge/SKProductBridge.m | 60 ++++++ .../SKProductDiscountBridge.h | 23 +++ .../SKProductDiscountBridge.m | 29 +++ .../SKProductSubscriptionPeriodBridge.h | 12 ++ .../SKProductSubscriptionPeriodBridge.m | 13 ++ .../StoreKitBridges/UADSStoreKitReflection.h | 8 + .../StoreKitBridges/UADSStoreKitReflection.m | 10 + .../UADSProducRequestDelegateAdapter.h | 10 + .../UADSProducRequestDelegateAdapter.m | 25 +++ .../UADSSKProductReader/UADSSKProductReader.h | 18 ++ .../UADSSKProductReader/UADSSKProductReader.m | 67 +++++++ .../UADSTransactionObserver.h | 13 ++ .../UADSTransactionObserver.m | 44 +++++ .../Store/Transactions/USTRProductRequest.h | 12 -- .../Store/Transactions/USTRProductRequest.m | 155 --------------- .../Transactions/USTRTransactionObserver.h | 5 - .../Transactions/USTRTransactionObserver.m | 54 ------ .../Store/UADSAppStoreReceiptReader.h | 14 ++ .../Store/UADSAppStoreReceiptReader.m | 12 ++ UnityServices/Store/USTRStore.h | 23 ++- UnityServices/Store/USTRStore.m | 79 ++++++-- .../Categories/NSArray/NSArray + Map.h | 11 ++ .../Categories/NSArray/NSArray + Map.m | 11 ++ .../Categories/NSArray/NSArray+Convenience.h | 11 ++ .../Categories/NSArray/NSArray+Convenience.m | 16 ++ .../Categories/NSDate/NSDate + NSNumber.h | 10 + .../Categories/NSDate/NSDate + NSNumber.m | 7 + .../NSEnumWrapper/NSPrimitivesBox.h | 18 ++ .../NSEnumWrapper/NSPrimitivesBox.m | 36 ++++ .../NSInvocation/NSInvocation+Convenience.h | 58 ++++++ .../NSInvocation/NSInvocation+Convenience.m | 79 ++++++++ .../NSMutableDictionary + SafeOperations.h | 7 + .../NSMutableDictionary + SafeOperations.m | 10 + .../NSObject/NSObject+Convenience.h | 9 + .../NSObject/NSObject+Convenience.m | 30 +++ .../UADSProxyReflection/UADSProxyReflection.h | 125 +++++++++++++ .../UADSProxyReflection/UADSProxyReflection.m | 177 ++++++++++++++++++ UnityServices/UADSTools/UADSTools.h | 36 ++++ UnityServices/UADSTools/UADSTools.m | 11 ++ 78 files changed, 2260 insertions(+), 263 deletions(-) create mode 100644 UnityAdsTests/Categories/NSDate/NSDate_NSNumberTests.m create mode 100644 UnityAdsTests/Categories/NSMutableDictionary + SafeOperations/NSMutableDictionary_SafeOperationsTests.m create mode 100644 UnityAdsTests/UADSTools/Categories/NSInvocation/Mocks/NSInvocationTargetMock.h create mode 100644 UnityAdsTests/UADSTools/Categories/NSInvocation/Mocks/NSInvocationTargetMock.m create mode 100644 UnityAdsTests/UADSTools/Categories/NSInvocation/NSInvocationTests.m create mode 100644 UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSBridgeMock.h create mode 100644 UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSBridgeMock.m create mode 100644 UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSProxyReflectionMock.h create mode 100644 UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSProxyReflectionMock.m create mode 100644 UnityAdsTests/UADSTools/UADSProxyReflection/UADSProxyReflectionTests.m create mode 100644 UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/NSLocale + CustomInit.h create mode 100644 UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/NSLocale + CustomInit.m create mode 100644 UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProduct + CustomInit.h create mode 100644 UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProduct + CustomInit.m create mode 100644 UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductDiscount + CustomInit.h create mode 100644 UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductDiscount + CustomInit.m create mode 100644 UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductSubscriptionPeriod + CustomInit.h create mode 100644 UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductSubscriptionPeriod + CustomInit.m create mode 100644 UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/SKProductBridgeTests.m create mode 100644 UnityServices/Store/Transactions/Categories/NSLocale/NSLocale + PriceDictionary/NSLocale + PriceDictionary.h create mode 100644 UnityServices/Store/Transactions/Categories/NSLocale/NSLocale + PriceDictionary/NSLocale + PriceDictionary.m create mode 100644 UnityServices/Store/Transactions/Categories/SKPayment/SKPayment + Dictionary/SKPayment + Dictionary.h create mode 100644 UnityServices/Store/Transactions/Categories/SKPayment/SKPayment + Dictionary/SKPayment + Dictionary.m create mode 100644 UnityServices/Store/Transactions/Categories/SKPaymentTransaction/SKPaymentTransaction + Dictionary/SKPaymentTransaction + Dictionary.h create mode 100644 UnityServices/Store/Transactions/Categories/SKPaymentTransaction/SKPaymentTransaction + Dictionary/SKPaymentTransaction + Dictionary.m create mode 100644 UnityServices/Store/Transactions/Categories/SKProductBridge/SKProductBridge + Dictionary/SKProductBridge + Dictionary.h create mode 100644 UnityServices/Store/Transactions/Categories/SKProductBridge/SKProductBridge + Dictionary/SKProductBridge + Dictionary.m create mode 100644 UnityServices/Store/Transactions/Categories/SKProductDiscountBridge/SKProductDiscountBridge + Dictionary/SKProductDiscountBridge + Dictionary.h create mode 100644 UnityServices/Store/Transactions/Categories/SKProductDiscountBridge/SKProductDiscountBridge + Dictionary/SKProductDiscountBridge + Dictionary.m create mode 100644 UnityServices/Store/Transactions/Categories/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge + Dictionary/SKProductSubscriptionPeriodBridge + Dictionary.h create mode 100644 UnityServices/Store/Transactions/Categories/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge + Dictionary/SKProductSubscriptionPeriodBridge + Dictionary.m create mode 100644 UnityServices/Store/Transactions/Categories/SKRequest/SKRequest + UniqueID/SKProductsRequest+UniqueID.h create mode 100644 UnityServices/Store/Transactions/Categories/SKRequest/SKRequest + UniqueID/SKProductsRequest+UniqueID.m create mode 100644 UnityServices/Store/Transactions/StoreKitBridges/SKProductBridge/SKProductBridge.h create mode 100644 UnityServices/Store/Transactions/StoreKitBridges/SKProductBridge/SKProductBridge.m create mode 100644 UnityServices/Store/Transactions/StoreKitBridges/SKProductDiscountBridge/SKProductDiscountBridge.h create mode 100644 UnityServices/Store/Transactions/StoreKitBridges/SKProductDiscountBridge/SKProductDiscountBridge.m create mode 100644 UnityServices/Store/Transactions/StoreKitBridges/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge.h create mode 100644 UnityServices/Store/Transactions/StoreKitBridges/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge.m create mode 100644 UnityServices/Store/Transactions/StoreKitBridges/UADSStoreKitReflection.h create mode 100644 UnityServices/Store/Transactions/StoreKitBridges/UADSStoreKitReflection.m create mode 100644 UnityServices/Store/Transactions/UADSSKProductReader/UADSProducRequestDelegateAdapter.h create mode 100644 UnityServices/Store/Transactions/UADSSKProductReader/UADSProducRequestDelegateAdapter.m create mode 100644 UnityServices/Store/Transactions/UADSSKProductReader/UADSSKProductReader.h create mode 100644 UnityServices/Store/Transactions/UADSSKProductReader/UADSSKProductReader.m create mode 100644 UnityServices/Store/Transactions/UADSTransactionObserver/UADSTransactionObserver.h create mode 100644 UnityServices/Store/Transactions/UADSTransactionObserver/UADSTransactionObserver.m delete mode 100644 UnityServices/Store/Transactions/USTRProductRequest.h delete mode 100644 UnityServices/Store/Transactions/USTRProductRequest.m delete mode 100644 UnityServices/Store/Transactions/USTRTransactionObserver.h delete mode 100644 UnityServices/Store/Transactions/USTRTransactionObserver.m create mode 100644 UnityServices/Store/UADSAppStoreReceiptReader.h create mode 100644 UnityServices/Store/UADSAppStoreReceiptReader.m create mode 100644 UnityServices/UADSTools/Categories/NSArray/NSArray + Map.h create mode 100644 UnityServices/UADSTools/Categories/NSArray/NSArray + Map.m create mode 100644 UnityServices/UADSTools/Categories/NSArray/NSArray+Convenience.h create mode 100644 UnityServices/UADSTools/Categories/NSArray/NSArray+Convenience.m create mode 100644 UnityServices/UADSTools/Categories/NSDate/NSDate + NSNumber.h create mode 100644 UnityServices/UADSTools/Categories/NSDate/NSDate + NSNumber.m create mode 100644 UnityServices/UADSTools/Categories/NSInvocation/NSEnumWrapper/NSPrimitivesBox.h create mode 100644 UnityServices/UADSTools/Categories/NSInvocation/NSEnumWrapper/NSPrimitivesBox.m create mode 100644 UnityServices/UADSTools/Categories/NSInvocation/NSInvocation+Convenience.h create mode 100644 UnityServices/UADSTools/Categories/NSInvocation/NSInvocation+Convenience.m create mode 100644 UnityServices/UADSTools/Categories/NSMutableDictionary/NSMutableDictionary + SafeOperations/NSMutableDictionary + SafeOperations.h create mode 100644 UnityServices/UADSTools/Categories/NSMutableDictionary/NSMutableDictionary + SafeOperations/NSMutableDictionary + SafeOperations.m create mode 100644 UnityServices/UADSTools/Categories/NSObject/NSObject+Convenience.h create mode 100644 UnityServices/UADSTools/Categories/NSObject/NSObject+Convenience.m create mode 100644 UnityServices/UADSTools/UADSProxyReflection/UADSProxyReflection.h create mode 100644 UnityServices/UADSTools/UADSProxyReflection/UADSProxyReflection.m create mode 100644 UnityServices/UADSTools/UADSTools.h create mode 100644 UnityServices/UADSTools/UADSTools.m diff --git a/UnityAds.podspec b/UnityAds.podspec index 6b3806ad..05c103db 100644 --- a/UnityAds.podspec +++ b/UnityAds.podspec @@ -1,12 +1,12 @@ Pod::Spec.new do |s| s.name = 'UnityAds' - s.version = '3.6.0' + s.version = '3.6.2' s.license = { :type => 'Unity License', :file => 'LICENSE' } s.author = { 'UnityAds' => 'itunes@unity3d.com' } s.homepage = 'https://unity3d.com/services/ads' s.summary = 'Monetize your entire player base and reach new audiences with video ads.' s.platform = :ios - s.source = { :http => 'https://github.com/Unity-Technologies/unity-ads-ios/releases/download/3.6.0/UnityAds.framework.zip' } + s.source = { :http => 'https://github.com/Unity-Technologies/unity-ads-ios/releases/download/3.6.2/UnityAds.framework.zip' } s.ios.deployment_target = '9.0' s.ios.vendored_frameworks = 'UnityAds.framework' s.ios.xcconfig = { 'OTHER_LDFLAGS' => '-framework UnityAds' } diff --git a/UnityAds/PrefixHeader.pch b/UnityAds/PrefixHeader.pch index bd645540..9e2338f8 100644 --- a/UnityAds/PrefixHeader.pch +++ b/UnityAds/PrefixHeader.pch @@ -3,5 +3,6 @@ #import #import "USRVDeviceLog.h" +#import "UADSTools.h" #endif /* PrefixHeader_pch */ diff --git a/UnityAdsTests/Categories/NSDate/NSDate_NSNumberTests.m b/UnityAdsTests/Categories/NSDate/NSDate_NSNumberTests.m new file mode 100644 index 00000000..172dc2f2 --- /dev/null +++ b/UnityAdsTests/Categories/NSDate/NSDate_NSNumberTests.m @@ -0,0 +1,20 @@ +#import +#import "NSDate + NSNumber.h" + +@interface NSDate_NSNumberTests : XCTestCase + +@end + +@implementation NSDate_NSNumberTests + +-(void)test_returns_timestamp_as_ns_number { + XCTAssertTrue([[NSDate new].uads_timeIntervalSince1970 isKindOfClass:[NSNumber class]]); +} + +-(void)test_returns_correct_double_value { + NSDate *date = [NSDate dateWithTimeIntervalSince1970: 100.100]; + NSNumber *timestamp = date.uads_timeIntervalSince1970; + XCTAssertEqual(date.timeIntervalSince1970, timestamp.doubleValue); +} + +@end diff --git a/UnityAdsTests/Categories/NSMutableDictionary + SafeOperations/NSMutableDictionary_SafeOperationsTests.m b/UnityAdsTests/Categories/NSMutableDictionary + SafeOperations/NSMutableDictionary_SafeOperationsTests.m new file mode 100644 index 00000000..dfe32574 --- /dev/null +++ b/UnityAdsTests/Categories/NSMutableDictionary + SafeOperations/NSMutableDictionary_SafeOperationsTests.m @@ -0,0 +1,28 @@ +#import +#import "NSMutableDictionary + SafeOperations.h" + +@interface NSMutableDictionary_SafeOperationsTests : XCTestCase + +@end + +@implementation NSMutableDictionary_SafeOperationsTests + + +-(void)test_dictionary_doesnt_set_nil_object_nor_set_the_key { + NSMutableDictionary* dictionary = [[NSMutableDictionary alloc] init]; + NSString *testKey = @"Test"; + [dictionary uads_setValueIfNotNil: nil forKey: testKey]; + XCTAssertEqual(dictionary.allKeys.count, 0); +} + +-(void)test_dictionary_set_nonnul_object { + NSMutableDictionary* dictionary = [[NSMutableDictionary alloc] init]; + NSString *testKey = @"Test"; + NSNumber *testValue = @3; + [dictionary uads_setValueIfNotNil: testValue forKey: testKey]; + XCTAssertEqual(dictionary.allKeys.count, 1); + XCTAssertEqualObjects(dictionary[testKey], testValue); +} + + +@end diff --git a/UnityAdsTests/UADSTools/Categories/NSInvocation/Mocks/NSInvocationTargetMock.h b/UnityAdsTests/UADSTools/Categories/NSInvocation/Mocks/NSInvocationTargetMock.h new file mode 100644 index 00000000..d77ed169 --- /dev/null +++ b/UnityAdsTests/UADSTools/Categories/NSInvocation/Mocks/NSInvocationTargetMock.h @@ -0,0 +1,37 @@ + +#import + +#define NSINVOCATION_MOCK_RETURNED_VALUE @10 +typedef NS_ENUM(NSInteger, NSInvocationTarget) { + NSInvocationTargetArgument1, + NSInvocationTargetArgument2 +}; + +NS_ASSUME_NONNULL_BEGIN + +@interface NSInvocationTargetMock : NSObject + + +@property (nonatomic) NSNumber * getNumberArgument; +@property (nonatomic) NSNumber * mockFunctionArgument; +@property (nonatomic) NSInvocationTarget enumArgument; +@property (nonatomic) double doubleValue; +@property (class,nonatomic,readonly) NSNumber * getNumberArgument; +@property (class,nonatomic,readonly) NSNumber * mockFunctionArgument; + + +-(NSNumber *)getNumberWithArg: (NSNumber *) number; ++(NSNumber *)getNumberWithArg: (NSNumber *) number; + +-(void)mockFunctionWithArg: (NSNumber *) number; ++(void)mockFunctionWithArg: (NSNumber *) number; + +-(void)mockFunctionWithEnumArg: (NSInvocationTarget) type; + + +-(void)callWithDouble: (double) value; + ++(void)reset; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityAdsTests/UADSTools/Categories/NSInvocation/Mocks/NSInvocationTargetMock.m b/UnityAdsTests/UADSTools/Categories/NSInvocation/Mocks/NSInvocationTargetMock.m new file mode 100644 index 00000000..bd04982e --- /dev/null +++ b/UnityAdsTests/UADSTools/Categories/NSInvocation/Mocks/NSInvocationTargetMock.m @@ -0,0 +1,51 @@ +#import "NSInvocationTargetMock.h" + +@implementation NSInvocationTargetMock +@synthesize doubleValue; +@synthesize getNumberArgument; +@synthesize mockFunctionArgument; +@synthesize enumArgument; +static NSNumber * getNumberArgument; +static NSNumber * mockFunctionArgument; + ++(NSNumber *)getNumberWithArg: (NSNumber *)number { + getNumberArgument = number; + return NSINVOCATION_MOCK_RETURNED_VALUE; +} ++(NSNumber *)getNumberArgument { + return getNumberArgument; +} + ++(NSNumber *)mockFunctionArgument { + return mockFunctionArgument; +} + ++ (void)reset { + getNumberArgument = 0; + mockFunctionArgument = 0; +} + ++(void)mockFunctionWithArg: (NSNumber *)number { + mockFunctionArgument = number; +} + +-(void)mockFunctionWithArg: (NSNumber *)number { + self.mockFunctionArgument = number; +} + +-(NSNumber *)getNumberWithArg: (NSNumber *)number { + self.getNumberArgument = number; + return NSINVOCATION_MOCK_RETURNED_VALUE; +} + + +- (void)mockFunctionWithEnumArg:(NSInvocationTarget)type { + self.enumArgument = type; +} + +- (void)callWithDouble:(double)value { + self.doubleValue = value; +} + + +@end diff --git a/UnityAdsTests/UADSTools/Categories/NSInvocation/NSInvocationTests.m b/UnityAdsTests/UADSTools/Categories/NSInvocation/NSInvocationTests.m new file mode 100644 index 00000000..ab90e549 --- /dev/null +++ b/UnityAdsTests/UADSTools/Categories/NSInvocation/NSInvocationTests.m @@ -0,0 +1,114 @@ +#import "NSInvocationTargetMock.h" +#import "NSInvocation+Convenience.h" +#import +#import "NSPrimitivesBox.h" +#define TEST_PASSED_VALUE @5 +@interface NSInvocationTests : XCTestCase + +@end + +@implementation NSInvocationTests + +- (void)setUp { + [NSInvocationTargetMock reset]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)test_executes_instance_function_with_an_argument { + NSInvocationTargetMock *mock = [NSInvocationTargetMock new]; + [NSInvocation uads_invokeUsingMethod: @"mockFunctionWithArg:" + classType: [NSInvocationTargetMock class] + target: mock + args: @[TEST_PASSED_VALUE]]; + XCTAssertEqualObjects(mock.mockFunctionArgument, TEST_PASSED_VALUE); +} + +- (void)test_executes_class_function_with_an_argument { + [NSInvocation uads_invokeUsingMethod: @"mockFunctionWithArg:" + classType: [NSInvocationTargetMock class] + target: nil + args: @[TEST_PASSED_VALUE]]; + XCTAssertEqualObjects(NSInvocationTargetMock.mockFunctionArgument, TEST_PASSED_VALUE); +} + + +- (void)test_executes_instance_function_with_returned_value_with_an_argument { + NSInvocationTargetMock *mock = [NSInvocationTargetMock new]; + NSNumber *returnedValue = [NSInvocation uads_invokeWithReturnedUsingMethod: @"getNumberWithArg:" + classType: [NSInvocationTargetMock class] + target: mock + args: @[TEST_PASSED_VALUE]]; + XCTAssertEqualObjects(returnedValue, NSINVOCATION_MOCK_RETURNED_VALUE); +} + +- (void)test_executes_class_function_with_returned_value_and_argument { + NSNumber *returnedValue = [NSInvocation uads_invokeWithReturnedUsingMethod: @"getNumberWithArg:" + classType: [NSInvocationTargetMock class] + target: nil + args: @[TEST_PASSED_VALUE]]; + XCTAssertEqualObjects(returnedValue, NSINVOCATION_MOCK_RETURNED_VALUE); +} + +- (void)test_passes_enum_to_the_invocation { + NSInvocationTargetMock *mock = [NSInvocationTargetMock new]; + NSInvocationTarget arg = NSInvocationTargetArgument1; + NSPrimitivesBox *box = [NSPrimitivesBox newWithBytes:&arg objCType:@encode(NSInvocationTarget)]; + [NSInvocation uads_invokeUsingMethod: @"mockFunctionWithArg:" + classType: [NSInvocationTargetMock class] + target: mock + args: @[box]]; + XCTAssertEqual(mock.enumArgument, arg); +} + + +- (void)test_invocation_can_call_using_double { + NSInvocationTargetMock *mock = [NSInvocationTargetMock new]; + double arg = 10.5; + NSPrimitivesBox *box = [NSPrimitivesBox newWithBytes: &arg objCType: @encode(double)]; + [NSInvocation uads_invokeUsingMethod: @"callWithDouble:" + classType: [NSInvocationTargetMock class] + target: mock + args: @[box]]; + XCTAssertEqual(mock.doubleValue, arg); +} + + +- (void)test_calls_non_existed_selector_on_instance_should_resist_to_crash { + NSInvocationTargetMock *mock = [NSInvocationTargetMock new]; + [NSInvocation uads_invokeUsingMethod: @"selectorThatDoesntExist:" + classType: [NSInvocationTargetMock class] + target: mock + args: @[]]; +} + + +- (void)test_calls_non_existed_selector_on_class_should_resist_to_crash { + [NSInvocation uads_invokeUsingMethod: @"selectorThatDoesntExist:" + classType: [NSInvocationTargetMock class] + target: nil + args: @[]]; +} + + + +- (void)test_calls_non_existed_selector_on_instance_should_return_nil { + NSInvocationTargetMock *mock = [NSInvocationTargetMock new]; + id val = [NSInvocation uads_invokeWithReturnedUsingMethod: @"selectorThatDoesntExist:" + classType: [NSInvocationTargetMock class] + target: mock + args: @[]]; + XCTAssertNil(val); +} + + +- (void)test_calls_non_existed_selector_on_class_should_return_nil { + id val = [NSInvocation uads_invokeWithReturnedUsingMethod: @"selectorThatDoesntExist:" + classType: [NSInvocationTargetMock class] + target: nil + args: @[]]; + XCTAssertNil(val); +} + + + +@end diff --git a/UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSBridgeMock.h b/UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSBridgeMock.h new file mode 100644 index 00000000..9841bd36 --- /dev/null +++ b/UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSBridgeMock.h @@ -0,0 +1,16 @@ +#import +#import "UADSProxyReflection.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface UADSBridgeMock: UADSProxyReflection +@property (nonatomic, readonly) NSString *testValue; ++ (instancetype)createDefault; +- (NSString *)nonExistingKVO; ++ (void)setMockSelectors: (NSArray *)names; ++ (void)setMockKeys: (NSArray *)names; +@end + +NS_ASSUME_NONNULL_END + + diff --git a/UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSBridgeMock.m b/UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSBridgeMock.m new file mode 100644 index 00000000..f9aecd88 --- /dev/null +++ b/UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSBridgeMock.m @@ -0,0 +1,64 @@ +#import "UADSBridgeMock.h" + +NSArray *mockedSelectors; +NSArray *mockKeysForKVO; + +@interface ClosedClassMock: NSObject +@end + +@implementation ClosedClassMock +@end + +@interface UADSBridgeMock() +@end + + +@implementation UADSBridgeMock + +- (NSString *)testValue { + return @"TEST_VALUE"; +} + ++(NSString *)className { + return @"UADSBridgeMock"; +} + ++ (instancetype)createDefault { + return [UADSBridgeMock getProxyWithObject: [ClosedClassMock new]]; +} + +- (NSString *)nonExistingKVO { + return [self valueForKey: @"NonExisted"]; +} + ++ (NSArray *)requiredSelectors { + return mockedSelectors; +} + ++ (NSArray *)requiredKeysForKVO { + return mockKeysForKVO; +} + ++ (void)setMockKeys:(NSArray *)names { + mockKeysForKVO = names; +} + ++ (void)setMockSelectors:(NSArray *)names { + mockedSelectors = names; +} + +- (void)fakeSelectorToTest { + // do nothing, just allow test to see this selector +} + + + +@end + + + + + + + + diff --git a/UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSProxyReflectionMock.h b/UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSProxyReflectionMock.h new file mode 100644 index 00000000..134b9f7e --- /dev/null +++ b/UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSProxyReflectionMock.h @@ -0,0 +1,9 @@ +#import +#import "UADSProxyReflection.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface UADSProxyReflectionMock: UADSProxyReflection +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSProxyReflectionMock.m b/UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSProxyReflectionMock.m new file mode 100644 index 00000000..47b53b32 --- /dev/null +++ b/UnityAdsTests/UADSTools/UADSProxyReflection/Mocks/UADSProxyReflectionMock.m @@ -0,0 +1,8 @@ + +#import "UADSProxyReflectionMock.h" + +@implementation UADSProxyReflectionMock ++ (NSString *)className { + return @"NSMutableArray"; +} +@end diff --git a/UnityAdsTests/UADSTools/UADSProxyReflection/UADSProxyReflectionTests.m b/UnityAdsTests/UADSTools/UADSProxyReflection/UADSProxyReflectionTests.m new file mode 100644 index 00000000..05130fd8 --- /dev/null +++ b/UnityAdsTests/UADSTools/UADSProxyReflection/UADSProxyReflectionTests.m @@ -0,0 +1,115 @@ +#import +#import "NSPrimitivesBox.h" +#import "UADSProxyReflectionMock.h" +#import "UADSBridgeMock.h" + +NSString * const TEST_VALUE = @"TEST_STRING"; + +@interface UADSProxyReflectionTests: XCTestCase +@property (nonatomic, strong) UADSProxyReflectionMock* sut; +@property (nonatomic, strong) UADSProxyReflection* sutEmpty; +@end + +@implementation UADSProxyReflectionTests + +- (void)setUp { + _sut = [UADSProxyReflectionMock getProxyWithObject: TEST_VALUE]; + _sutEmpty = [UADSProxyReflection getProxyWithObject: TEST_VALUE]; +} +- (void)tearDown { + [UADSBridgeMock setMockKeys: @[]]; + [UADSBridgeMock setMockSelectors: @[]]; +} + +-(void)test_returns_proper_class_nsstring { + XCTAssertEqualObjects([NSMutableArray class], [UADSProxyReflectionMock getClass]); +} + +-(void)test_returns_nil_for_unavailable { + XCTAssertNil([UADSProxyReflection getClass]); +} + +-(void)test_returns_exist_for_available_class { + XCTAssertTrue([UADSProxyReflectionMock exists]); +} + +-(void)test_returns_non_exist_for_available_class { + XCTAssertFalse([UADSProxyReflection exists]); +} + +-(void)test_can_initialize_using_reflection_class_method { + XCTAssertEqualObjects(self.defaultMock.proxyObject, @[TEST_VALUE]); +} + +-(void)test_should_not_crash_or_go_into_the_loop_if_value_for_key_is_not_supported { + UADSProxyReflection *object = [UADSProxyReflection getProxyWithObject: [NSArray new]]; + XCTAssertEqualObjects([object valueForKey: @"This selector doesnt exist"], nil); +} + +-(void)test_calls_method_without_arguments_using_reflection_api { + UADSProxyReflectionMock *newObject = self.defaultMock; + [newObject callInstanceMethod:@"removeAllObjects" args:@[]]; + XCTAssertEqualObjects(newObject.proxyObject, @[]); +} + +-(void)test_calls_method_value_for_key { + UADSBridgeMock *newObject = [UADSBridgeMock createDefault]; + NSString *obj = [newObject nonExistingKVO]; + XCTAssertNil(obj); +} + +- (void)test_calls_non_existed_method { + NSString *newObject = (NSString *)self.defaultMock; + XCTAssertEqual(newObject.intValue, 0); +} + +-(void)test_calls_method_with_arguments_using_reflection_api { + UADSProxyReflectionMock *newObject = self.defaultMock; + NSInteger value = 0; + NSPrimitivesBox *index = [NSPrimitivesBox newWithBytes: &value objCType:@encode(NSInteger)]; + [newObject callInstanceMethod:@"removeObjectAtIndex:" args:@[index]]; + XCTAssertEqualObjects(newObject.proxyObject, @[]); +} + + +- (void)test_proxy_returns_same_result { + NSNumber *numberOne = @1; + id proxy = [UADSProxyReflectionMock getProxyWithObject: numberOne]; + XCTAssertEqualObjects([numberOne stringValue], + [proxy stringValue], + @"proxy MUST return same result as object"); +} + +- (void)test_exist_returns_true_if_selector_is_present { + [UADSBridgeMock setMockSelectors: @[@"fakeSelectorToTest"] ]; + XCTAssertTrue(UADSBridgeMock.exists); +} + +- (void)test_exist_returns_false_if_selector_is_present { + [UADSBridgeMock setMockSelectors: @[@"fakeSelectorToTest", @"selector_is_not_present"] ]; + XCTAssertFalse(UADSBridgeMock.exists); +} + +- (void)test_exist_returns_true_if_a_property_is_present { + [UADSBridgeMock setMockKeys: @[@"testValue"] ]; + XCTAssertTrue(UADSBridgeMock.exists); +} + +- (void)test_exist_returns_false_if_a_property_is_not_present { + [UADSBridgeMock setMockKeys: @[@"value_not_present"] ]; + XCTAssertFalse(UADSBridgeMock.exists); +} + +- (void)test_value_for_key_passes_to_proxy_object_and_returns_value { + UADSBridgeMock *newObject = [UADSBridgeMock createDefault]; + id proxy = [UADSProxyReflectionMock getProxyWithObject: newObject]; + XCTAssertEqualObjects([proxy valueForKey: @"testValue"], + @"TEST_VALUE"); +} + +-(UADSProxyReflectionMock *)defaultMock { + return [UADSProxyReflectionMock getInstanceUsingClassMethod: @"arrayWithObject:" + args: @[TEST_VALUE]]; +} + +@end diff --git a/UnityAdsTests/UnityAdsTests-Bridging-Header.h b/UnityAdsTests/UnityAdsTests-Bridging-Header.h index 3511c04a..7dab62de 100644 --- a/UnityAdsTests/UnityAdsTests-Bridging-Header.h +++ b/UnityAdsTests/UnityAdsTests-Bridging-Header.h @@ -32,6 +32,7 @@ #import "UADSVideoPlayerHandler.h" #import "USRVWebRequestWithUrlConnection.h" #import "USRVWebRequestFactory.h" +#import "UADSProxyReflection.h" #import "UADSTokenStorage.h" // METADATA diff --git a/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/NSLocale + CustomInit.h b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/NSLocale + CustomInit.h new file mode 100644 index 00000000..4eba986c --- /dev/null +++ b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/NSLocale + CustomInit.h @@ -0,0 +1,10 @@ +#import +#import "NSLocale + PriceDictionary.h" +NS_ASSUME_NONNULL_BEGIN + +@interface NSLocale(CustomInit) ++(instancetype)newForUS; ++(NSDictionary *)defaultTestData; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/NSLocale + CustomInit.m b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/NSLocale + CustomInit.m new file mode 100644 index 00000000..661866fd --- /dev/null +++ b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/NSLocale + CustomInit.m @@ -0,0 +1,16 @@ +#import "NSLocale + CustomInit.h" + +@implementation NSLocale(CustomInit) ++ (instancetype)newForUS { + NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier: @"en_US"]; + return locale; +} + ++ (NSDictionary *)defaultTestData { + NSMutableDictionary *mDictionary = [NSMutableDictionary new]; + mDictionary[kNSLocaleCountryCodeKey] = @"US"; + mDictionary[kNSLocaleCurrencySymbolKey] = @"$"; + mDictionary[kNSLocaleCurrencyCodeKey] = @"USD"; + return mDictionary; +} +@end diff --git a/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProduct + CustomInit.h b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProduct + CustomInit.h new file mode 100644 index 00000000..2dd164e2 --- /dev/null +++ b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProduct + CustomInit.h @@ -0,0 +1,10 @@ +#import +#import "SKProductBridge + Dictionary.h" +NS_ASSUME_NONNULL_BEGIN + +@interface SKProduct(CustomInit) ++(instancetype)newFromDictionary: (NSDictionary *)dictionary; ++(NSDictionary *)defaultTestData; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProduct + CustomInit.m b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProduct + CustomInit.m new file mode 100644 index 00000000..ef539029 --- /dev/null +++ b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProduct + CustomInit.m @@ -0,0 +1,73 @@ +#import "SKProduct + CustomInit.h" +#import "NSLocale + CustomInit.h" +#import "SKProductSubscriptionPeriod + CustomInit.h" +#import "SKProductDiscount + CustomInit.h" +#import "NSArray + Map.h" + +@implementation SKProduct(CustomInit) ++ (instancetype)newFromDictionary:(NSDictionary *)dictionary { + + + SKProduct *newProduct = [[SKProduct alloc] init]; + + [newProduct setValue: dictionary[kProductIdentifierKey] + forKey: kProductIdentifierKey]; + + [newProduct setValue: dictionary[kLocalizedTitleKey] + forKey: kLocalizedTitleKey]; + + [newProduct setValue: dictionary[kLocalizedDescriptionKey] + forKey: kLocalizedDescriptionKey]; + + [newProduct setValue: dictionary[kPriceKey] + forKey: kPriceKey]; + + [newProduct setValue: [NSLocale newForUS] + forKey: kPriceLocaleKey]; + + if (@available(iOS 11.2, *)) { + NSDictionary *periodDictionary = dictionary[kSubscriptionPeriodKey]; + [newProduct setValue: [SKProductSubscriptionPeriod newFromDictionary: periodDictionary] + forKey: kSubscriptionPeriodKey]; + + + NSDictionary *introductoryPriceDictionary = dictionary[kIntroductoryPriceKey]; + + [newProduct setValue: [SKProductDiscount newFromDictionary:introductoryPriceDictionary] + forKey: kIntroductoryPriceKey]; + + + } + if (@available(iOS 12.2, *)) { + NSArray *discountsDictionaries = dictionary[kDiscountsKey]; + NSArray *discounts = [discountsDictionaries uads_mapObjectsUsingBlock:^id _Nonnull(NSDictionary * _Nonnull obj) { + return [SKProductDiscount newFromDictionary: obj]; + }]; + + [newProduct setValue: discounts forKey:kDiscountsKey]; + } + + return newProduct; +} + + ++ (NSDictionary *)defaultTestData { + NSMutableDictionary *mDictionary = [NSMutableDictionary new]; + mDictionary[kProductIdentifierKey] = @"ProductIdentifier"; + mDictionary[kLocalizedTitleKey] = @"LocalizedTitle"; + mDictionary[kLocalizedDescriptionKey] = @"LocalizedDescription"; + mDictionary[kPriceKey] = [NSNumber numberWithDouble: 100.9]; + mDictionary[kPriceLocaleKey] = [NSLocale defaultTestData]; + mDictionary[kIsFamilyShareableKey] = [NSNumber numberWithBool: false]; + mDictionary[kIsDownloadableKey] = [NSNumber numberWithBool: false]; + + if (@available(iOS 11.2, *)) { + mDictionary[kSubscriptionPeriodKey] = [SKProductSubscriptionPeriod defaultTestData]; + mDictionary[kIntroductoryPriceKey] = [SKProductDiscount defaultTestData]; + mDictionary[kDiscountsKey] = @[[SKProductDiscount defaultTestData]]; + } + + return mDictionary; + +} +@end diff --git a/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductDiscount + CustomInit.h b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductDiscount + CustomInit.h new file mode 100644 index 00000000..3b698f14 --- /dev/null +++ b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductDiscount + CustomInit.h @@ -0,0 +1,10 @@ +#import +#import "SKProductDiscountBridge.h" +NS_ASSUME_NONNULL_BEGIN + +@interface SKProductDiscount(CustomInit) ++(instancetype)newFromDictionary: (NSDictionary *)dictionary; ++(NSDictionary *)defaultTestData; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductDiscount + CustomInit.m b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductDiscount + CustomInit.m new file mode 100644 index 00000000..478127a2 --- /dev/null +++ b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductDiscount + CustomInit.m @@ -0,0 +1,55 @@ +#import "SKProductDiscount + CustomInit.h" +#import "SKProductSubscriptionPeriod + CustomInit.h" +#import "NSLocale + CustomInit.h" + +@implementation SKProductDiscount(CustomInit) + ++(instancetype)newFromDictionary: (NSDictionary *)dictionary { + SKProductDiscount *newPeriod = [[SKProductDiscount alloc] init]; + + if (@available(iOS 12.0, *)) { + [newPeriod setValue: dictionary[kSKProductDiscountIDKey] + forKey: kSKProductDiscountIDKey]; + } + + SKProductSubscriptionPeriod *period = [SKProductSubscriptionPeriod newFromDictionary: dictionary[kSKProductDiscountSubscriptionPeriodKey]]; + + [newPeriod setValue: period + forKey: kSKProductDiscountSubscriptionPeriodKey]; + + [newPeriod setValue: [NSLocale newForUS] + forKey: kSKProductDiscountPriceLocaleKey]; + + [newPeriod setValue: dictionary[kSKProductDiscountPriceKey] + forKey: kSKProductDiscountPriceKey]; + + [newPeriod setValue: dictionary[kSKProductDiscountPaymentModeKey] + forKey: kSKProductDiscountPaymentModeKey]; + + [newPeriod setValue: dictionary[kSKProductDiscountNumberOfPeriodsKey] + forKey: kSKProductDiscountNumberOfPeriodsKey]; + + if (@available(iOS 12.2, *)) { + [newPeriod setValue: dictionary[kSKProductDiscountTypeKey] + forKey: kSKProductDiscountTypeKey]; + } + return newPeriod; +} + ++(NSDictionary *)defaultTestData { + NSMutableDictionary *mDictionary = [NSMutableDictionary new]; + mDictionary[kSKProductDiscountIDKey] = @"SKProductDiscountID"; + mDictionary[kSKProductDiscountSubscriptionPeriodKey] = [SKProductSubscriptionPeriod defaultTestData]; + mDictionary[kSKProductDiscountPriceKey] = @100.5; + mDictionary[kSKProductDiscountNumberOfPeriodsKey] = @1; + mDictionary[kSKProductDiscountPaymentModeKey] = @2; + if (@available(iOS 12.2, *)) { + mDictionary[kSKProductDiscountTypeKey] = @1; + } else { + mDictionary[kSKProductDiscountTypeKey] = nil; + } + + return mDictionary; +} + +@end diff --git a/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductSubscriptionPeriod + CustomInit.h b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductSubscriptionPeriod + CustomInit.h new file mode 100644 index 00000000..67be85d2 --- /dev/null +++ b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductSubscriptionPeriod + CustomInit.h @@ -0,0 +1,11 @@ +#import +#import "SKProductSubscriptionPeriodBridge.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SKProductSubscriptionPeriod(CustomInit) ++(instancetype)newFromDictionary: (NSDictionary *)dictionary; ++(NSDictionary *)defaultTestData; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductSubscriptionPeriod + CustomInit.m b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductSubscriptionPeriod + CustomInit.m new file mode 100644 index 00000000..a4efee62 --- /dev/null +++ b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/Mocks/SKProductSubscriptionPeriod + CustomInit.m @@ -0,0 +1,22 @@ + +#import "SKProductSubscriptionPeriod + CustomInit.h" + +@implementation SKProductSubscriptionPeriod(CustomInit) + ++ (instancetype)newFromDictionary:(NSDictionary *)dictionary { + SKProductSubscriptionPeriod *newPeriod = [[SKProductSubscriptionPeriod alloc] init]; + [newPeriod setValue: dictionary[kSKProductSubscriptionUnitKey] + forKey: kSKProductSubscriptionUnitKey]; + + [newPeriod setValue: dictionary[kSKProductSubscriptionNumberOfUnitsKey] + forKey: kSKProductSubscriptionNumberOfUnitsKey]; + return newPeriod; +} + ++ (NSDictionary *)defaultTestData { + NSMutableDictionary *mDictionary = [NSMutableDictionary new]; + mDictionary[kSKProductSubscriptionUnitKey] = @1; + mDictionary[kSKProductSubscriptionNumberOfUnitsKey] = @1; + return mDictionary; +} +@end diff --git a/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/SKProductBridgeTests.m b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/SKProductBridgeTests.m new file mode 100644 index 00000000..164b5e93 --- /dev/null +++ b/UnityAdsTests/UnityServices/Store/Transactions/StoreKitBridges/SKProductBridgeTests.m @@ -0,0 +1,145 @@ +#import +#import "SKProductBridge + Dictionary.h" +#import "SKProduct + CustomInit.h" +#import "NSLocale + CustomInit.h" +#import "SKProductSubscriptionPeriod + CustomInit.h" +#import "SKProductDiscount + CustomInit.h" + +#define FALSE_AS_NUMBER [NSNumber numberWithBool: false] + +@interface SKProductBridgeTests : XCTestCase + +@end + +@implementation SKProductBridgeTests + + +- (void)test_returns_proper_identifier { + XCTAssertEqualObjects(self.defaultMock.productIdentifier, + self.skProductDictionary[kProductIdentifierKey]); +} + +- (void)test_returns_proper_localized_title { + + XCTAssertEqualObjects(self.defaultMock.localizedTitle, + self.skProductDictionary[kLocalizedTitleKey]); +} + +- (void)test_returns_proper_localized_description { + + XCTAssertEqualObjects(self.defaultMock.localizedDescription, + self.skProductDictionary[kLocalizedDescriptionKey]); +} + +- (void)test_returns_proper_price_locale { + [self compareNSLocale: self.defaultMock.priceLocale + withExpectedData: self.nsLocaleDictionary]; +} + +- (void)test_access_isDownloadable_doesnt_crash { + XCTAssertEqualObjects(self.defaultMock.isDownloadableNumber, + FALSE_AS_NUMBER); +} + + +- (void)test_access_isFamilySharable_doesnt_crash { + XCTAssertEqualObjects(self.defaultMock.isDownloadableNumber, + FALSE_AS_NUMBER); +} + +- (void)test_access_subscription_identifier_doesnt_crash { + XCTAssertEqualObjects(self.defaultMock.subscriptionGroupIdentifier, + nil); +} + +- (void)test_returns_proper_number_of_units { + XCTAssertEqualObjects(self.defaultMock.subscriptionPeriod.numberOfUnitsNumber, + self.skPeriodDictionary[kSKProductSubscriptionNumberOfUnitsKey]); +} + +- (void)test_returns_proper_units { + XCTAssertEqualObjects(self.defaultMock.subscriptionPeriod.unitNumber, + self.skPeriodDictionary[kSKProductSubscriptionUnitKey]); +} + +- (void)test_returns_proper_price { + XCTAssertEqualObjects(self.defaultMock.price, + self.skProductDictionary[kSKProductDiscountPriceKey]); +} + +- (void)test_returns_proper_introductory_price_locale { + if (@available(iOS 11.2, *)) { + [self compareProductDiscount: self.defaultMock.introductoryPrice + withExpectedData: self.skProductDiscountDictionary]; + } +} + +- (void)test_return_proper_discounts { + + [self.defaultMock.discounts enumerateObjectsUsingBlock:^(SKProductDiscountBridge * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [self compareProductDiscount: obj + withExpectedData: self.skProductDiscountDictionary]; + + }]; + +} + +- (void)compareNSLocale: (NSLocale *)locale + withExpectedData: (NSDictionary *)dictionary { + if (@available(iOS 12.0, *)) { + XCTAssertEqualObjects(locale.currencyCode, + dictionary[kNSLocaleCurrencyCodeKey]); + XCTAssertEqualObjects(locale.currencySymbol, + dictionary[kNSLocaleCurrencySymbolKey]); + XCTAssertEqualObjects(locale.countryCode, + dictionary[kNSLocaleCountryCodeKey]); + } +} + +- (void)compareProductDiscount: (SKProductDiscountBridge *)discount + withExpectedData: (NSDictionary *)dictionary { + [self compareNSLocale: discount.priceLocale + withExpectedData: self.nsLocaleDictionary]; + if (@available(iOS 12.0, *)) { + XCTAssertEqualObjects(discount.identifier, dictionary[kSKProductDiscountIDKey]); + } else { + XCTAssertEqualObjects(discount.identifier, nil); + } + + XCTAssertEqualObjects(discount.price, dictionary[kSKProductDiscountPriceKey]); + XCTAssertEqualObjects(discount.numberOfPeriodsNumber, dictionary[kSKProductDiscountNumberOfPeriodsKey]); + XCTAssertEqualObjects(discount.paymentModeNumber, dictionary[kSKProductDiscountPaymentModeKey]); + XCTAssertEqualObjects(discount.typeNumber, dictionary[kSKProductDiscountTypeKey]); +} + +- (SKProductBridge *)defaultMock { + SKProduct *mock = [SKProduct newFromDictionary: self.skProductDictionary]; + return [SKProductBridge getProxyWithObject: mock];; +} + +- (NSDictionary *)nsLocaleDictionary { + return [NSLocale defaultTestData]; +} + +- (NSDictionary *)skProductDictionary { + return [SKProduct defaultTestData]; +} + +- (NSDictionary *)skPeriodDictionary { + if (@available(iOS 11.2, *)) { + return [SKProductSubscriptionPeriod defaultTestData]; + } else { + return nil; + } +} + +-(NSDictionary *)skProductDiscountDictionary { + if (@available(iOS 11.2, *)) { + return [SKProductDiscount defaultTestData]; + } else { + return nil; + } +} + + +@end diff --git a/UnityServices/Core/Properties/USRVSdkProperties.m b/UnityServices/Core/Properties/USRVSdkProperties.m index b904adb4..6fb72bfd 100644 --- a/UnityServices/Core/Properties/USRVSdkProperties.m +++ b/UnityServices/Core/Properties/USRVSdkProperties.m @@ -7,12 +7,12 @@ NSString * const kUnityServicesLocalStorageFilePrefix = @"UnityAdsStorage-"; NSString * const kUnityServicesWebviewBranchInfoDictionaryKey = @"UADSWebviewBranch"; NSString * const kUnityServicesWebviewConfigInfoDictionaryKey = @"UADSWebviewConfig"; -NSString * const kUnityServicesVersionName = @"3.6.0"; +NSString * const kUnityServicesVersionName = @"3.6.2"; NSString * const kUnityServicesFlavorDebug = @"debug"; NSString * const kUnityServicesFlavorRelease = @"release"; NSString * const kChinaIsoAlpha2Code = @"CN"; NSString * const kChinaIsoAlpha3Code = @"CHN"; -int const kUnityServicesVersionCode = 3600; +int const kUnityServicesVersionCode = 3620; @implementation USRVSdkProperties diff --git a/UnityServices/Store/Api/USTRApiProducts.m b/UnityServices/Store/Api/USTRApiProducts.m index 723863e0..d61739b7 100644 --- a/UnityServices/Store/Api/USTRApiProducts.m +++ b/UnityServices/Store/Api/USTRApiProducts.m @@ -1,32 +1,66 @@ #import "USTRApiProducts.h" #import "USRVWebViewCallback.h" #import "USTRStore.h" +#import "USRVWebViewApp.h" + +NSString *const kPRODUCT_REQUEST_COMPLETE_EVENT_TYPE = @"PRODUCT_REQUEST_COMPLETE"; +NSString *const kPRODUCT_REQUEST_FAILED_EVENT_TYPE = @"PRODUCT_REQUEST_FAILED"; +NSString *const kTRANSACTION_RECEIVED_EVENT_TYPE = @"RECEIVED_TRANSACTION"; +NSString *const kPRODUCT_REQUEST_NO_PRODUCTS_EVENT_TYPE = @"PRODUCT_REQUEST_ERROR_NO_PRODUCTS"; +NSString *const kSTORE_EVENT_CATEGORY = @"STORE"; +NSString *const kRECEIPT_ERROR_STRING = @"NO_RECEIPT"; + @implementation USTRApiProducts ++(USTRStore *)facade { + return USTRStore.sharedInstance; +} + ++(USRVWebViewApp *)eventSender { + return [USRVWebViewApp getCurrentApp]; +} + + (void)WebViewExposed_requestProductInfos:(NSArray *)productIds requestId:(NSNumber *)requestId callback:(USRVWebViewCallback *)callback { - [USTRStore requestProductInfos:productIds requestId:requestId]; + + [self.facade getProductsUsingIDs:productIds success:^(NSArray *products) { + + NSString *eventType = products.count > 0 ? kPRODUCT_REQUEST_COMPLETE_EVENT_TYPE : kPRODUCT_REQUEST_NO_PRODUCTS_EVENT_TYPE; + + [self.eventSender sendEvent: eventType + category: kSTORE_EVENT_CATEGORY + param1: requestId, products, nil]; + } onError:^(NSError * _Nonnull error) { + + [self.eventSender sendEvent: kPRODUCT_REQUEST_FAILED_EVENT_TYPE + category: kSTORE_EVENT_CATEGORY + param1: requestId, [error description], nil]; + }]; + [callback invoke:nil]; } + (void)WebViewExposed_startTransactionObserver:(USRVWebViewCallback *)callback { - [USTRStore startTransactionObserver]; + [self.facade startTransactionObserverWithCompletion:^(NSArray * _Nonnull result) { + [self.eventSender sendEvent: kTRANSACTION_RECEIVED_EVENT_TYPE + category: kSTORE_EVENT_CATEGORY + param1: result, nil]; + }]; [callback invoke:nil]; } + (void)WebViewExposed_stopTransactionObserver:(USRVWebViewCallback *)callback { - [USTRStore stopTransactionObserver]; + [self.facade stopTransactionObserver]; [callback invoke:nil]; } + (void)WebViewExposed_getReceipt:(USRVWebViewCallback *)callback { - NSData* receipt = [USTRStore getReceipt]; - if (receipt) { - NSString *encodedReceipt = [receipt base64EncodedStringWithOptions:0]; - [callback invoke:encodedReceipt, nil]; + NSString *encodedReceipt = self.facade.encodedReceipt; + if (encodedReceipt) { + [callback invoke: encodedReceipt, nil]; } else { - [callback error:@"NO_RECEIPT" arg1:nil]; + [callback error: kRECEIPT_ERROR_STRING arg1:nil]; } } diff --git a/UnityServices/Store/Transactions/Categories/NSLocale/NSLocale + PriceDictionary/NSLocale + PriceDictionary.h b/UnityServices/Store/Transactions/Categories/NSLocale/NSLocale + PriceDictionary/NSLocale + PriceDictionary.h new file mode 100644 index 00000000..e1796e5a --- /dev/null +++ b/UnityServices/Store/Transactions/Categories/NSLocale/NSLocale + PriceDictionary/NSLocale + PriceDictionary.h @@ -0,0 +1,10 @@ +NS_ASSUME_NONNULL_BEGIN +static NSString *const kNSLocaleCurrencySymbolKey = @"currencySymbol"; +static NSString *const kNSLocaleCountryCodeKey = @"countryCode"; +static NSString *const kNSLocaleCurrencyCodeKey = @"currencyCode"; + +@interface NSLocale(PriceDictionary) +-(NSDictionary* _Nonnull )uads_Dictionary; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/Transactions/Categories/NSLocale/NSLocale + PriceDictionary/NSLocale + PriceDictionary.m b/UnityServices/Store/Transactions/Categories/NSLocale/NSLocale + PriceDictionary/NSLocale + PriceDictionary.m new file mode 100644 index 00000000..bd485649 --- /dev/null +++ b/UnityServices/Store/Transactions/Categories/NSLocale/NSLocale + PriceDictionary/NSLocale + PriceDictionary.m @@ -0,0 +1,20 @@ +#import "NSLocale + PriceDictionary.h" +#import "NSMutableDictionary + SafeOperations.h" + +@implementation NSLocale(PriceDictionary) +- (NSDictionary *)uads_Dictionary { + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + + [dictionary uads_setValueIfNotNil: [self valueForKey: kNSLocaleCurrencySymbolKey] + forKey: kNSLocaleCurrencySymbolKey]; + + [dictionary uads_setValueIfNotNil: [self valueForKey: kNSLocaleCountryCodeKey] + forKey: kNSLocaleCountryCodeKey]; + + [dictionary uads_setValueIfNotNil: [self valueForKey: kNSLocaleCurrencyCodeKey] + forKey: kNSLocaleCurrencyCodeKey]; + + + return dictionary; +} +@end diff --git a/UnityServices/Store/Transactions/Categories/SKPayment/SKPayment + Dictionary/SKPayment + Dictionary.h b/UnityServices/Store/Transactions/Categories/SKPayment/SKPayment + Dictionary/SKPayment + Dictionary.h new file mode 100644 index 00000000..99fa45e2 --- /dev/null +++ b/UnityServices/Store/Transactions/Categories/SKPayment/SKPayment + Dictionary/SKPayment + Dictionary.h @@ -0,0 +1,9 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SKPayment(Dictionary) +-(NSDictionary* _Nonnull )uads_Dictionary; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/Transactions/Categories/SKPayment/SKPayment + Dictionary/SKPayment + Dictionary.m b/UnityServices/Store/Transactions/Categories/SKPayment/SKPayment + Dictionary/SKPayment + Dictionary.m new file mode 100644 index 00000000..a028ed07 --- /dev/null +++ b/UnityServices/Store/Transactions/Categories/SKPayment/SKPayment + Dictionary/SKPayment + Dictionary.m @@ -0,0 +1,25 @@ +#import "SKPayment + Dictionary.h" +#import "NSMutableDictionary + SafeOperations.h" + +static NSString *const kSKPaymentProductIDKey = @"productIdentifier"; +static NSString *const kSKPaymentQuantityKey = @"quantity"; +static NSString *const kSKPaymentApplicationUsernameKey = @"applicationUsername"; + +@implementation SKPayment(Dictionary) + +-(NSDictionary* _Nonnull )uads_Dictionary { + + NSMutableDictionary *paymentDictionary = [[NSMutableDictionary alloc] init]; + [paymentDictionary uads_setValueIfNotNil: self.productIdentifier + forKey: kSKPaymentProductIDKey]; + + [paymentDictionary uads_setValueIfNotNil: self.applicationUsername + forKey: kSKPaymentApplicationUsernameKey]; + + [paymentDictionary uads_setValueIfNotNil: [NSNumber numberWithInteger: self.quantity] + forKey: kSKPaymentQuantityKey]; + + return paymentDictionary; +} + +@end diff --git a/UnityServices/Store/Transactions/Categories/SKPaymentTransaction/SKPaymentTransaction + Dictionary/SKPaymentTransaction + Dictionary.h b/UnityServices/Store/Transactions/Categories/SKPaymentTransaction/SKPaymentTransaction + Dictionary/SKPaymentTransaction + Dictionary.h new file mode 100644 index 00000000..fe4077b6 --- /dev/null +++ b/UnityServices/Store/Transactions/Categories/SKPaymentTransaction/SKPaymentTransaction + Dictionary/SKPaymentTransaction + Dictionary.h @@ -0,0 +1,9 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SKPaymentTransaction(Dictionary) +-(NSDictionary* _Nonnull )uads_Dictionary; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/Transactions/Categories/SKPaymentTransaction/SKPaymentTransaction + Dictionary/SKPaymentTransaction + Dictionary.m b/UnityServices/Store/Transactions/Categories/SKPaymentTransaction/SKPaymentTransaction + Dictionary/SKPaymentTransaction + Dictionary.m new file mode 100644 index 00000000..55e7f4e1 --- /dev/null +++ b/UnityServices/Store/Transactions/Categories/SKPaymentTransaction/SKPaymentTransaction + Dictionary/SKPaymentTransaction + Dictionary.m @@ -0,0 +1,35 @@ +#import "SKPayment + Dictionary.h" +#import "SKPaymentTransaction + Dictionary.h" +#import "NSMutableDictionary + SafeOperations.h" +#import "NSDate + NSNumber.h" + + +static NSString *const kSKPaymentTransactionStateKey = @"transactionState"; +static NSString *const kSKPaymentTransactionDateKey = @"transactionDate"; +static NSString *const kSKPaymentTransactionIDKey = @"transactionIdentifier"; +static NSString *const kSKPaymentOriginalTransactionKey = @"originalTransaction"; +static NSString *const kSKPaymentTransactionPaymentKey = @"payment"; + +@implementation SKPaymentTransaction(Dictionary) + +- (NSDictionary *)uads_Dictionary { + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + + [dictionary uads_setValueIfNotNil: self.transactionIdentifier + forKey: kSKPaymentTransactionIDKey]; + + [dictionary uads_setValueIfNotNil: [NSNumber numberWithInteger: self.transactionState] + forKey: kSKPaymentTransactionStateKey]; + + [dictionary uads_setValueIfNotNil: self.transactionDate.uads_timeIntervalSince1970 + forKey: kSKPaymentTransactionDateKey]; + + [dictionary uads_setValueIfNotNil: self.originalTransaction.uads_Dictionary + forKey: kSKPaymentOriginalTransactionKey]; + + [dictionary uads_setValueIfNotNil: self.payment.uads_Dictionary + forKey: kSKPaymentTransactionPaymentKey]; + + return dictionary; +} +@end diff --git a/UnityServices/Store/Transactions/Categories/SKProductBridge/SKProductBridge + Dictionary/SKProductBridge + Dictionary.h b/UnityServices/Store/Transactions/Categories/SKProductBridge/SKProductBridge + Dictionary/SKProductBridge + Dictionary.h new file mode 100644 index 00000000..9d98e72f --- /dev/null +++ b/UnityServices/Store/Transactions/Categories/SKProductBridge/SKProductBridge + Dictionary/SKProductBridge + Dictionary.h @@ -0,0 +1,8 @@ +#import "SKProductBridge.h" +NS_ASSUME_NONNULL_BEGIN + +@interface SKProductBridge(Dictionary) +-(NSDictionary* _Nonnull )uads_Dictionary; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/Transactions/Categories/SKProductBridge/SKProductBridge + Dictionary/SKProductBridge + Dictionary.m b/UnityServices/Store/Transactions/Categories/SKProductBridge/SKProductBridge + Dictionary/SKProductBridge + Dictionary.m new file mode 100644 index 00000000..e043f7b0 --- /dev/null +++ b/UnityServices/Store/Transactions/Categories/SKProductBridge/SKProductBridge + Dictionary/SKProductBridge + Dictionary.m @@ -0,0 +1,68 @@ +#import "SKProductBridge + Dictionary.h" +#import "NSMutableDictionary + SafeOperations.h" +#import "SKProductSubscriptionPeriodBridge + Dictionary.h" +#import "NSLocale + PriceDictionary.h" +#import "SKProductDiscountBridge + Dictionary.h" +#import "NSArray + Map.h" + +static NSString *const kSKProductIDLocalizedTitleKey = @"localizedTitle"; +static NSString *const kSKProductIDLocalizedDescriptionKey = @"localizedDescription"; +static NSString *const kSKProductIDKey = @"productIdentifier"; +static NSString *const kSKProductIsDownloadableKey = @"downloadable"; +static NSString *const kSKProductIsFamilySharable = @"familySharable"; +static NSString *const kSKProductSubscriptionGroupIdentifierKey = @"subscriptionGroupIdentifier"; +static NSString *const kSKProductSubscriptionPeriodKey = @"subscriptionPeriod"; +static NSString *const kSKProductPriceKey = @"price"; +static NSString *const kSKProductPriceLocalKey = @"priceLocale"; +static NSString *const kSKProductDiscountsKey = @"discounts"; +static NSString *const kSKProductIntroductoryPriceKey = @"introductoryPrice"; + +@implementation SKProductBridge(Dictionary) + +-(NSDictionary* _Nonnull )uads_Dictionary { + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + + [dictionary uads_setValueIfNotNil: self.productIdentifier + forKey: kSKProductIDKey]; + + [dictionary uads_setValueIfNotNil: self.localizedTitle + forKey: kSKProductIDLocalizedTitleKey]; + + [dictionary uads_setValueIfNotNil: self.localizedDescription + forKey: kSKProductIDLocalizedDescriptionKey]; + + [dictionary uads_setValueIfNotNil: self.isDownloadableNumber + forKey: kSKProductIsDownloadableKey]; + + [dictionary uads_setValueIfNotNil: self.isFamilyShareableNumber + forKey: kSKProductIsFamilySharable]; + + [dictionary uads_setValueIfNotNil: self.subscriptionGroupIdentifier + forKey: kSKProductSubscriptionGroupIdentifierKey]; + + [dictionary uads_setValueIfNotNil: self.subscriptionPeriod.uads_Dictionary + forKey: kSKProductSubscriptionPeriodKey]; + + [dictionary uads_setValueIfNotNil: self.introductoryPrice.uads_Dictionary + forKey: kSKProductIntroductoryPriceKey]; + + [dictionary uads_setValueIfNotNil: self.price forKey: kSKProductPriceKey]; + + [dictionary uads_setValueIfNotNil: self.priceLocale.uads_Dictionary + forKey: kSKProductPriceLocalKey]; + + [dictionary uads_setValueIfNotNil: self.discountsAsDictionaries + forKey: kSKProductDiscountsKey]; + + return dictionary; +} + +- (NSArray *)discountsAsDictionaries { + NSArray *arrayOfDiscounts = self.discounts; + NULL_CHECK_OR_RETURN_NIL(arrayOfDiscounts) + return [arrayOfDiscounts uads_mapObjectsUsingBlock: ^id _Nonnull(SKProductDiscountBridge* _Nonnull obj) { + return obj.uads_Dictionary; + }]; +} + +@end diff --git a/UnityServices/Store/Transactions/Categories/SKProductDiscountBridge/SKProductDiscountBridge + Dictionary/SKProductDiscountBridge + Dictionary.h b/UnityServices/Store/Transactions/Categories/SKProductDiscountBridge/SKProductDiscountBridge + Dictionary/SKProductDiscountBridge + Dictionary.h new file mode 100644 index 00000000..3aae8903 --- /dev/null +++ b/UnityServices/Store/Transactions/Categories/SKProductDiscountBridge/SKProductDiscountBridge + Dictionary/SKProductDiscountBridge + Dictionary.h @@ -0,0 +1,9 @@ +#import +#import "SKProductDiscountBridge.h" +NS_ASSUME_NONNULL_BEGIN + +@interface SKProductDiscountBridge(Dictionary) +-(NSDictionary* _Nonnull )uads_Dictionary; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/Transactions/Categories/SKProductDiscountBridge/SKProductDiscountBridge + Dictionary/SKProductDiscountBridge + Dictionary.m b/UnityServices/Store/Transactions/Categories/SKProductDiscountBridge/SKProductDiscountBridge + Dictionary/SKProductDiscountBridge + Dictionary.m new file mode 100644 index 00000000..756c3606 --- /dev/null +++ b/UnityServices/Store/Transactions/Categories/SKProductDiscountBridge/SKProductDiscountBridge + Dictionary/SKProductDiscountBridge + Dictionary.m @@ -0,0 +1,34 @@ +#import "SKProductDiscountBridge + Dictionary.h" +#import "NSMutableDictionary + SafeOperations.h" +#import "SKProductSubscriptionPeriodBridge + Dictionary.h" +#import "NSLocale + PriceDictionary.h" + + +@implementation SKProductDiscountBridge(Dictionary) +-(NSDictionary* _Nonnull )uads_Dictionary { + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + + [dictionary uads_setValueIfNotNil: self.subscriptionPeriod.uads_Dictionary + forKey: kSKProductDiscountSubscriptionPeriodKey]; + + [dictionary uads_setValueIfNotNil: self.identifier + forKey: kSKProductDiscountIDKey]; + + [dictionary uads_setValueIfNotNil: self.priceLocale.uads_Dictionary + forKey: kSKProductDiscountPriceLocaleKey]; + + [dictionary uads_setValueIfNotNil: self.price + forKey: kSKProductDiscountPriceKey]; + + [dictionary uads_setValueIfNotNil: self.paymentModeNumber + forKey: kSKProductDiscountPaymentModeKey]; + + [dictionary uads_setValueIfNotNil: self.numberOfPeriodsNumber + forKey: kSKProductDiscountNumberOfPeriodsKey]; + + [dictionary uads_setValueIfNotNil: self.typeNumber + forKey: kSKProductDiscountTypeKey]; + + return dictionary; +} +@end diff --git a/UnityServices/Store/Transactions/Categories/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge + Dictionary/SKProductSubscriptionPeriodBridge + Dictionary.h b/UnityServices/Store/Transactions/Categories/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge + Dictionary/SKProductSubscriptionPeriodBridge + Dictionary.h new file mode 100644 index 00000000..93388375 --- /dev/null +++ b/UnityServices/Store/Transactions/Categories/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge + Dictionary/SKProductSubscriptionPeriodBridge + Dictionary.h @@ -0,0 +1,9 @@ +#import +#import "SKProductSubscriptionPeriodBridge.h" +NS_ASSUME_NONNULL_BEGIN + +@interface SKProductSubscriptionPeriodBridge(Dictionary) +-(NSDictionary* _Nonnull )uads_Dictionary; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/Transactions/Categories/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge + Dictionary/SKProductSubscriptionPeriodBridge + Dictionary.m b/UnityServices/Store/Transactions/Categories/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge + Dictionary/SKProductSubscriptionPeriodBridge + Dictionary.m new file mode 100644 index 00000000..58316109 --- /dev/null +++ b/UnityServices/Store/Transactions/Categories/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge + Dictionary/SKProductSubscriptionPeriodBridge + Dictionary.m @@ -0,0 +1,15 @@ +#import "SKProductSubscriptionPeriodBridge + Dictionary.h" +#import "NSMutableDictionary + SafeOperations.h" + +@implementation SKProductSubscriptionPeriodBridge(Dictionary) +-(NSDictionary* _Nonnull )uads_Dictionary { + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + + [dictionary uads_setValueIfNotNil: self.numberOfUnitsNumber + forKey: kSKProductSubscriptionNumberOfUnitsKey]; + + [dictionary uads_setValueIfNotNil: self.unitNumber + forKey: kSKProductSubscriptionUnitKey]; + return dictionary; +} +@end diff --git a/UnityServices/Store/Transactions/Categories/SKRequest/SKRequest + UniqueID/SKProductsRequest+UniqueID.h b/UnityServices/Store/Transactions/Categories/SKRequest/SKRequest + UniqueID/SKProductsRequest+UniqueID.h new file mode 100644 index 00000000..a2ab2d3f --- /dev/null +++ b/UnityServices/Store/Transactions/Categories/SKRequest/SKRequest + UniqueID/SKProductsRequest+UniqueID.h @@ -0,0 +1,9 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SKProductsRequest(Category) +-(NSString *)uniqueID; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/Transactions/Categories/SKRequest/SKRequest + UniqueID/SKProductsRequest+UniqueID.m b/UnityServices/Store/Transactions/Categories/SKRequest/SKRequest + UniqueID/SKProductsRequest+UniqueID.m new file mode 100644 index 00000000..dda5f85d --- /dev/null +++ b/UnityServices/Store/Transactions/Categories/SKRequest/SKRequest + UniqueID/SKProductsRequest+UniqueID.m @@ -0,0 +1,8 @@ +#import "SKProductsRequest+UniqueID.h" + +@implementation SKProductsRequest(Category) + +-(NSString *)uniqueID { + return [NSString stringWithFormat: @"%lu", (unsigned long)self.hash]; +} +@end diff --git a/UnityServices/Store/Transactions/StoreKitBridges/SKProductBridge/SKProductBridge.h b/UnityServices/Store/Transactions/StoreKitBridges/SKProductBridge/SKProductBridge.h new file mode 100644 index 00000000..304d915d --- /dev/null +++ b/UnityServices/Store/Transactions/StoreKitBridges/SKProductBridge/SKProductBridge.h @@ -0,0 +1,33 @@ +#import "UADSStoreKitReflection.h" +#import "SKProductSubscriptionPeriodBridge.h" +#import "SKProductDiscountBridge.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kProductIdentifierKey = @"productIdentifier"; +static NSString *const kLocalizedTitleKey = @"localizedTitle"; +static NSString *const kLocalizedDescriptionKey = @"localizedDescription"; +static NSString *const kIsFamilyShareableKey = @"isFamilyShareable"; +static NSString *const kIsDownloadableKey = @"isDownloadable"; +static NSString *const kSubscriptionGroupIdentifierKey = @"subscriptionGroupIdentifier"; +static NSString *const kPriceKey = @"price"; +static NSString *const kPriceLocaleKey = @"priceLocale"; +static NSString *const kSubscriptionPeriodKey = @"subscriptionPeriod"; +static NSString *const kIntroductoryPriceKey = @"introductoryPrice"; +static NSString *const kDiscountsKey = @"discounts"; + +@interface SKProductBridge : UADSStoreKitReflection +@property(nonatomic, readonly) NSString* productIdentifier; +@property(nonatomic, readonly) NSString* localizedDescription; +@property(nonatomic, readonly) NSString* localizedTitle; +@property(nonatomic, readonly) NSNumber* isDownloadableNumber; +@property(nonatomic, readonly) NSNumber* isFamilyShareableNumber; +@property(nonatomic, readonly) NSString* subscriptionGroupIdentifier; +@property(nonatomic, readonly) NSDecimalNumber *price; +@property(nonatomic, readonly) NSLocale* priceLocale; +@property(nonatomic, readonly, nullable) SKProductSubscriptionPeriodBridge *subscriptionPeriod; +@property(nonatomic, readonly, nullable) SKProductDiscountBridge *introductoryPrice; +@property(nonatomic, readonly, nullable) NSArray *discounts; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/Transactions/StoreKitBridges/SKProductBridge/SKProductBridge.m b/UnityServices/Store/Transactions/StoreKitBridges/SKProductBridge/SKProductBridge.m new file mode 100644 index 00000000..1a14d64b --- /dev/null +++ b/UnityServices/Store/Transactions/StoreKitBridges/SKProductBridge/SKProductBridge.m @@ -0,0 +1,60 @@ + +#import "SKProductBridge.h" +#import +#import "NSArray + Map.h" + +@implementation SKProductBridge + +- (NSString *)productIdentifier { + return [self valueForKey: kProductIdentifierKey]; +} + +- (NSString *)localizedTitle { + return [self valueForKey: kLocalizedTitleKey]; +} + +- (NSString *)localizedDescription { + return [self valueForKey: kLocalizedDescriptionKey]; +} + +- (NSNumber *)isDownloadableNumber { + return [self valueForKey: kIsDownloadableKey]; +} + +- (NSNumber *)isFamilyShareableNumber { + return [self valueForKey: kIsFamilyShareableKey]; +} + +- (NSString *)subscriptionGroupIdentifier { + return [self valueForKey: kSubscriptionGroupIdentifierKey]; +} + +- (NSDecimalNumber *)price { + return [self valueForKey: kPriceKey]; +} + +- (NSLocale *)priceLocale { + return [self valueForKey: kPriceLocaleKey]; +} + +- (SKProductDiscountBridge *)introductoryPrice { + id obj = [self valueForKey: kIntroductoryPriceKey]; + NULL_CHECK_OR_RETURN_NIL(obj) + return [SKProductDiscountBridge getProxyWithObject: obj]; +} +- (SKProductSubscriptionPeriodBridge *)subscriptionPeriod { + id obj = [self valueForKey: kSubscriptionPeriodKey]; + NULL_CHECK_OR_RETURN_NIL(obj) + return [SKProductSubscriptionPeriodBridge getProxyWithObject: obj]; +} + +- (NSArray *)discounts { + NSArray *discountsAsIDs = [self valueForKey: kDiscountsKey]; + NULL_CHECK_OR_RETURN_NIL(discountsAsIDs) + return [discountsAsIDs uads_mapObjectsUsingBlock:^id _Nonnull(id _Nonnull obj) { + return [SKProductDiscountBridge getProxyWithObject: obj]; + }]; +} + + +@end diff --git a/UnityServices/Store/Transactions/StoreKitBridges/SKProductDiscountBridge/SKProductDiscountBridge.h b/UnityServices/Store/Transactions/StoreKitBridges/SKProductDiscountBridge/SKProductDiscountBridge.h new file mode 100644 index 00000000..bfd6b929 --- /dev/null +++ b/UnityServices/Store/Transactions/StoreKitBridges/SKProductDiscountBridge/SKProductDiscountBridge.h @@ -0,0 +1,23 @@ +#import "UADSStoreKitReflection.h" +#import "SKProductSubscriptionPeriodBridge.h" +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kSKProductDiscountIDKey = @"identifier"; +static NSString *const kSKProductDiscountSubscriptionPeriodKey = @"subscriptionPeriod"; +static NSString *const kSKProductDiscountPriceLocaleKey = @"priceLocale"; +static NSString *const kSKProductDiscountPriceKey = @"price"; +static NSString *const kSKProductDiscountPaymentModeKey = @"paymentMode"; +static NSString *const kSKProductDiscountNumberOfPeriodsKey = @"numberOfPeriods"; +static NSString *const kSKProductDiscountTypeKey = @"type"; + +@interface SKProductDiscountBridge : UADSStoreKitReflection +@property(nonatomic, readonly) NSDecimalNumber *price; +@property(nonatomic, readonly) NSLocale *priceLocale; +@property(nonatomic, readonly, nullable) NSString *identifier; +@property(nonatomic, readonly) SKProductSubscriptionPeriodBridge *subscriptionPeriod; +@property(nonatomic, readonly) NSNumber *numberOfPeriodsNumber; +@property(nonatomic, readonly) NSNumber *paymentModeNumber; +@property(nonatomic, readonly) NSNumber *typeNumber; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/Transactions/StoreKitBridges/SKProductDiscountBridge/SKProductDiscountBridge.m b/UnityServices/Store/Transactions/StoreKitBridges/SKProductDiscountBridge/SKProductDiscountBridge.m new file mode 100644 index 00000000..783af881 --- /dev/null +++ b/UnityServices/Store/Transactions/StoreKitBridges/SKProductDiscountBridge/SKProductDiscountBridge.m @@ -0,0 +1,29 @@ +#import "SKProductDiscountBridge.h" + +@implementation SKProductDiscountBridge + +- (NSDecimalNumber *)price { + return [self valueForKey: kSKProductDiscountPriceKey]; +} + +- (NSString *)identifier { + return [self valueForKey: kSKProductDiscountIDKey]; +} + +- (NSLocale *)priceLocale { + return [self valueForKey: kSKProductDiscountPriceLocaleKey]; +} + +- (NSNumber *)numberOfPeriodsNumber { + return [self valueForKey: kSKProductDiscountNumberOfPeriodsKey]; +} + +- (NSNumber *)paymentModeNumber { + return [self valueForKey: kSKProductDiscountPaymentModeKey]; +} + +- (NSNumber *)typeNumber { + return [self valueForKey: kSKProductDiscountTypeKey]; +} + +@end diff --git a/UnityServices/Store/Transactions/StoreKitBridges/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge.h b/UnityServices/Store/Transactions/StoreKitBridges/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge.h new file mode 100644 index 00000000..4ff02316 --- /dev/null +++ b/UnityServices/Store/Transactions/StoreKitBridges/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge.h @@ -0,0 +1,12 @@ +#import "UADSStoreKitReflection.h" + +NS_ASSUME_NONNULL_BEGIN +static NSString *const kSKProductSubscriptionNumberOfUnitsKey = @"numberOfUnits"; +static NSString *const kSKProductSubscriptionUnitKey = @"unit"; + +@interface SKProductSubscriptionPeriodBridge: UADSStoreKitReflection +@property(nonatomic, readonly) NSNumber *numberOfUnitsNumber; +@property(nonatomic, readonly) NSNumber *unitNumber; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/Transactions/StoreKitBridges/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge.m b/UnityServices/Store/Transactions/StoreKitBridges/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge.m new file mode 100644 index 00000000..e3c6d02b --- /dev/null +++ b/UnityServices/Store/Transactions/StoreKitBridges/SKProductSubscriptionPeriodBridge/SKProductSubscriptionPeriodBridge.m @@ -0,0 +1,13 @@ +#import "SKProductSubscriptionPeriodBridge.h" + +@implementation SKProductSubscriptionPeriodBridge + +- (NSNumber *)unitNumber { + return [self valueForKey: kSKProductSubscriptionUnitKey]; +} + +- (NSNumber *)numberOfUnitsNumber { + return [self valueForKey: kSKProductSubscriptionNumberOfUnitsKey]; +} + +@end diff --git a/UnityServices/Store/Transactions/StoreKitBridges/UADSStoreKitReflection.h b/UnityServices/Store/Transactions/StoreKitBridges/UADSStoreKitReflection.h new file mode 100644 index 00000000..a57de19d --- /dev/null +++ b/UnityServices/Store/Transactions/StoreKitBridges/UADSStoreKitReflection.h @@ -0,0 +1,8 @@ +#import "UADSProxyReflection.h" +NS_ASSUME_NONNULL_BEGIN + +@interface UADSStoreKitReflection: UADSProxyReflection + +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/Transactions/StoreKitBridges/UADSStoreKitReflection.m b/UnityServices/Store/Transactions/StoreKitBridges/UADSStoreKitReflection.m new file mode 100644 index 00000000..bbf7d928 --- /dev/null +++ b/UnityServices/Store/Transactions/StoreKitBridges/UADSStoreKitReflection.m @@ -0,0 +1,10 @@ +#import "UADSStoreKitReflection.h" + +@implementation UADSStoreKitReflection +- (id)valueForKey:(NSString *)key { + //due to unexpected behaviour in SKProduct in sdk lower than 12.0 we have to return nil + //instead of calling super. + NULL_CHECK_OR_RETURN_NIL([self.proxyObject respondsToSelector: NSSelectorFromString(key)]); + return [self.proxyObject valueForKey: key]; +} +@end diff --git a/UnityServices/Store/Transactions/UADSSKProductReader/UADSProducRequestDelegateAdapter.h b/UnityServices/Store/Transactions/UADSSKProductReader/UADSProducRequestDelegateAdapter.h new file mode 100644 index 00000000..dbcad5f9 --- /dev/null +++ b/UnityServices/Store/Transactions/UADSSKProductReader/UADSProducRequestDelegateAdapter.h @@ -0,0 +1,10 @@ +#import +#import "UADSSKProductReader.h" +NS_ASSUME_NONNULL_BEGIN + +@interface UADSProducRequestDelegateAdapter : NSObject ++ (instancetype)newWithCompletion:(UADSSKProductReaderCompletion)completion + onErrorCompletion: (UADSSKProductReaderErrorCompletion) onError; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/Transactions/UADSSKProductReader/UADSProducRequestDelegateAdapter.m b/UnityServices/Store/Transactions/UADSSKProductReader/UADSProducRequestDelegateAdapter.m new file mode 100644 index 00000000..48ce26e4 --- /dev/null +++ b/UnityServices/Store/Transactions/UADSSKProductReader/UADSProducRequestDelegateAdapter.m @@ -0,0 +1,25 @@ +#import "UADSProducRequestDelegateAdapter.h" + +@interface UADSProducRequestDelegateAdapter() +@property (nonatomic, strong) UADSSKProductReaderCompletion completion; +@property (nonatomic, strong) UADSSKProductReaderErrorCompletion onError; +@end + +@implementation UADSProducRequestDelegateAdapter + ++ (instancetype)newWithCompletion:(nonnull UADSSKProductReaderCompletion)completion + onErrorCompletion:(nonnull UADSSKProductReaderErrorCompletion)onError { + UADSProducRequestDelegateAdapter* obj = [[self alloc] init]; + obj.completion = completion; + obj.onError = onError; + return obj; +} + +- (void)productsRequest:(SKProductsRequest *)request + didReceiveResponse:(SKProductsResponse *)response { + + NSArray* array = response.products ?: [[NSArray alloc] init]; + self.completion(array); +} + +@end diff --git a/UnityServices/Store/Transactions/UADSSKProductReader/UADSSKProductReader.h b/UnityServices/Store/Transactions/UADSSKProductReader/UADSSKProductReader.h new file mode 100644 index 00000000..31f34859 --- /dev/null +++ b/UnityServices/Store/Transactions/UADSSKProductReader/UADSSKProductReader.h @@ -0,0 +1,18 @@ +#import +NS_ASSUME_NONNULL_BEGIN + +typedef void(^UADSSKProductReaderCompletion)(NSArray *products); +typedef void(^UADSSKProductReaderErrorCompletion)(NSError *); + +@protocol UADSSKProductReader + +-(void)fetchProductsUsingIDS: (NSArray *)productIdentifiers + completion: (UADSSKProductReaderCompletion) completion + onErrorCompletion: (UADSSKProductReaderErrorCompletion) onError; + +@end + +@interface UADSSKProductReaderImp : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/Transactions/UADSSKProductReader/UADSSKProductReader.m b/UnityServices/Store/Transactions/UADSSKProductReader/UADSSKProductReader.m new file mode 100644 index 00000000..e4cd7b95 --- /dev/null +++ b/UnityServices/Store/Transactions/UADSSKProductReader/UADSSKProductReader.m @@ -0,0 +1,67 @@ +#import "UADSSKProductReader.h" +#import "UADSProducRequestDelegateAdapter.h" +#import "SKProductsRequest+UniqueID.h" + +@interface UADSSKProductReaderImp() +@property (nonatomic, strong) NSMutableDictionary* storage; +@property (nonatomic) dispatch_queue_t synchronizeQueue; +@property (nonatomic, strong) NSMutableDictionary* delegateStorage; +@end + +@implementation UADSSKProductReaderImp + +- (instancetype)init { + self = [super init]; + if (self) { + self.storage = [[NSMutableDictionary alloc] init]; + self.delegateStorage = [[NSMutableDictionary alloc] init]; + self.synchronizeQueue = dispatch_queue_create("UADSSKProductReader Queue", DISPATCH_QUEUE_SERIAL); + } + return self; +} + +- (void)fetchProductsUsingIDS:(NSArray *)productIdentifiers + completion:(UADSSKProductReaderCompletion)completion + onErrorCompletion:(nonnull UADSSKProductReaderErrorCompletion)onError { + + SKProductsRequest *productsRequest = [[SKProductsRequest alloc] + initWithProductIdentifiers:[NSSet setWithArray:productIdentifiers]]; + + __weak UADSSKProductReaderImp* weakSelf = self; + UADSProducRequestDelegateAdapter *delegate = [UADSProducRequestDelegateAdapter newWithCompletion:^(NSArray * _Nonnull products) { + + [weakSelf removeRequest: productsRequest]; + completion(products); + } + onErrorCompletion:^(NSError * _Nonnull error) { + + [weakSelf removeRequest: productsRequest]; + onError(error); + }]; + + [self storeRequest: productsRequest + andDelegate: delegate]; + + productsRequest.delegate = delegate; + + [productsRequest start]; +} + +-(void)storeRequest: (SKProductsRequest *)request + andDelegate: (UADSProducRequestDelegateAdapter *)delegate { + dispatch_sync(_synchronizeQueue, ^{ + self.storage[request.uniqueID] = request; + self.delegateStorage[request.uniqueID] = delegate; + }); +} + +-(void)removeRequest: (SKProductsRequest *)request { + dispatch_sync(_synchronizeQueue, ^{ + [self.storage removeObjectForKey: request.uniqueID]; + [self.delegateStorage removeObjectForKey: request.uniqueID]; + }); +} + +@end + + diff --git a/UnityServices/Store/Transactions/UADSTransactionObserver/UADSTransactionObserver.h b/UnityServices/Store/Transactions/UADSTransactionObserver/UADSTransactionObserver.h new file mode 100644 index 00000000..38acffc6 --- /dev/null +++ b/UnityServices/Store/Transactions/UADSTransactionObserver/UADSTransactionObserver.h @@ -0,0 +1,13 @@ +#import +#import "UADSAppStoreReceiptReader.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef void(^UADSTransactionObserverCompletion)(NSArray *result); + +@interface UADSTransactionObserver: NSObject ++(instancetype)newWithReceiptReader: (id) reader + andCompletion: (UADSTransactionObserverCompletion) completion; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/Transactions/UADSTransactionObserver/UADSTransactionObserver.m b/UnityServices/Store/Transactions/UADSTransactionObserver/UADSTransactionObserver.m new file mode 100644 index 00000000..b6a000bd --- /dev/null +++ b/UnityServices/Store/Transactions/UADSTransactionObserver/UADSTransactionObserver.m @@ -0,0 +1,44 @@ +#import "UADSTransactionObserver.h" +#import "NSMutableDictionary + SafeOperations.h" +#import "SKPaymentTransaction + Dictionary.h" +#import "NSArray + Map.h" +static NSString *const kSKPaymentTransactionReceiptKey = @"receipt"; + +@interface UADSTransactionObserver() +@property (nonatomic, nonnull, strong) UADSTransactionObserverCompletion completion; +@property (nonatomic, nonnull, strong) id reader; +@end + +@implementation UADSTransactionObserver + ++(instancetype)newWithReceiptReader: (id) reader + andCompletion: (UADSTransactionObserverCompletion) completion { + UADSTransactionObserver *obj = [[self alloc] init]; + obj.completion = completion; + obj.reader = reader; + return obj; +} + +- (void)paymentQueue:(nonnull SKPaymentQueue *)queue + updatedTransactions:(nonnull NSArray *)transactions { + + NSArray *transactionData = [transactions uads_mapObjectsUsingBlock:^id _Nonnull(id _Nonnull obj) { + return [self convertToDictionary: obj]; + }]; + + _completion(transactionData); + +} + +-(NSDictionary *)convertToDictionary: (SKPaymentTransaction *)transaction { + return [self attachReceiptTo: transaction.uads_Dictionary]; +} + +-(NSDictionary *)attachReceiptTo: (NSDictionary *)transactionData { + NSMutableDictionary *transactionMutable = [[NSMutableDictionary alloc] initWithDictionary: transactionData]; + [transactionMutable uads_setValueIfNotNil: self.reader.encodedReceipt + forKey: kSKPaymentTransactionReceiptKey]; + return transactionMutable; +} + +@end diff --git a/UnityServices/Store/Transactions/USTRProductRequest.h b/UnityServices/Store/Transactions/USTRProductRequest.h deleted file mode 100644 index 153e5218..00000000 --- a/UnityServices/Store/Transactions/USTRProductRequest.h +++ /dev/null @@ -1,12 +0,0 @@ -#import - -@interface USTRProductRequest : NSObject - -@property (nonatomic, strong) NSArray *productIds; -@property (nonatomic, strong) SKProductsRequest *currentRequest; -@property (nonatomic, strong) NSNumber *requestId; - -- (instancetype)initWithProductIds:(NSArray*)productIds requestId:(NSNumber*)requestId; -- (void)requestProducts; - -@end diff --git a/UnityServices/Store/Transactions/USTRProductRequest.m b/UnityServices/Store/Transactions/USTRProductRequest.m deleted file mode 100644 index 56048103..00000000 --- a/UnityServices/Store/Transactions/USTRProductRequest.m +++ /dev/null @@ -1,155 +0,0 @@ -#import "USTRProductRequest.h" -#import "USRVWebViewApp.h" - -@implementation USTRProductRequest - -static NSMutableArray *requests; - -- (instancetype)initWithProductIds:(NSArray*)productIds requestId:(NSNumber*)requestId { - self = [super init]; - - if (self) { - [self setProductIds:productIds]; - [self setRequestId:requestId]; - if (!requests) { - requests = [[NSMutableArray alloc] init]; - } - } - - return self; -} - -- (void)requestProducts { - NSSet *productIdentifiers = [NSSet setWithArray:self.productIds]; - [self setCurrentRequest:[[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers]]; - [[self currentRequest] setDelegate:self]; - [self addToActiveRequests]; - [[self currentRequest] start]; -} - -- (void)addToActiveRequests { - @synchronized (requests) { - [requests addObject:self]; - } -} - -- (void)removeFromActiveRequests { - @synchronized (requests) { - [requests removeObject:self]; - } -} - -- (void)sendProducts:(NSArray*)products invalidProducts:(NSArray*)invalidProducts { - if (products) { - int productCount = (int)products.count; - if (invalidProducts) { - productCount += invalidProducts.count; - } - - if (productCount < 1) { - [[USRVWebViewApp getCurrentApp] sendEvent:@"PRODUCT_REQUEST_ERROR_NO_PRODUCTS" category:@"STORE" param1:self.requestId, nil]; - return; - } - - NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; - for (SKProduct *product in products) { - if (product) { - NSMutableDictionary *productDict = [[NSMutableDictionary alloc] init]; - - if (product.isDownloadable) { - [productDict setObject:[NSNumber numberWithBool:product.isDownloadable] forKey:@"downloadable"]; - } - if (product.localizedTitle) { - [productDict setObject:product.localizedTitle forKey:@"localizedTitle"]; - } - if (product.localizedDescription) { - [productDict setObject:product.localizedDescription forKey:@"localizedDescription"]; - } - if (product.price) { - [productDict setObject:product.price forKey:@"price"]; - } - - NSMutableDictionary *priceLocaleDict = [[NSMutableDictionary alloc] init]; - - if (product.priceLocale && [product.priceLocale valueForKey:@"countryCode"]) { - [priceLocaleDict setObject:[product.priceLocale valueForKey:@"countryCode"] forKey:@"countryCode"]; - } - if (product.priceLocale && [product.priceLocale valueForKey:@"currencyCode"]) { - [priceLocaleDict setObject:[product.priceLocale valueForKey:@"currencyCode"] forKey:@"currencyCode"]; - } - if (product.priceLocale && [product.priceLocale valueForKey:@"currencySymbol"]) { - [priceLocaleDict setObject:[product.priceLocale valueForKey:@"currencySymbol"] forKey:@"currencySymbol"]; - } - - if (priceLocaleDict) { - [productDict setObject:priceLocaleDict forKey:@"priceLocale"]; - } - - if ([product valueForKey:@"subscriptionPeriod"]) { - id subscriptionPeriod = [product valueForKey:@"subscriptionPeriod"]; - NSUInteger numOfUnits = (NSUInteger)[subscriptionPeriod valueForKey:@"numberOfUnits"]; - BOOL isSubscription = (subscriptionPeriod != nil) && (numOfUnits > 0); - - if (isSubscription) { - NSMutableDictionary *subDictionary = [[NSMutableDictionary alloc] init]; - [subDictionary setObject:[NSNumber numberWithUnsignedInteger:numOfUnits] forKey:@"numberOfUnits"]; - - if ([subscriptionPeriod valueForKey:@"unit"]) { - NSNumber *periodUnit = [NSNumber numberWithUnsignedInteger:(NSUInteger)[subscriptionPeriod valueForKey:@"unit"]]; - [subDictionary setObject:periodUnit forKey:@"periodUnit"]; - } - - id introductoryPrice = [product valueForKey:@"introductoryPrice"]; - if (introductoryPrice) { - if ([introductoryPrice valueForKey:@"price"]) { - [subDictionary setObject:[introductoryPrice valueForKey:@"price"] forKey:@"introductoryPrice"]; - } - - if ([introductoryPrice valueForKey:@"subscriptionPeriod"]) { - id introductorySubscriptionPeriod = [introductoryPrice valueForKey:@"subscriptionPeriod"]; - if (introductorySubscriptionPeriod) { - if ([introductorySubscriptionPeriod valueForKey:@"unit"]) { - [subDictionary setObject:[introductorySubscriptionPeriod valueForKey:@"unit"] forKey:@"introductorySubPeriodUnit"]; - } - if ([introductorySubscriptionPeriod valueForKey:@"numberOfUnits"]) { - [subDictionary setObject:[introductorySubscriptionPeriod valueForKey:@"numberOfUnits"] forKey:@"introductorySubPeriodNumOfUnits"]; - } - } - } - } - - [productDict setObject:subDictionary forKey:@"subscription"]; - } - } - - [dictionary setObject:productDict forKey:product.productIdentifier]; - } - } - - [[USRVWebViewApp getCurrentApp] sendEvent:@"PRODUCT_REQUEST_COMPLETE" category:@"STORE" param1:self.requestId, dictionary, nil]; - } - else { - [[USRVWebViewApp getCurrentApp] sendEvent:@"PRODUCT_REQUEST_ERROR_NO_PRODUCTS" category:@"STORE" param1:self.requestId, nil]; - } -} - -/* DELEGATE */ - -- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { - NSArray *products = response.products; - NSArray *invalidProductIdentifiers = response.invalidProductIdentifiers; - - [self sendProducts:products invalidProducts:invalidProductIdentifiers]; -} - -- (void)requestDidFinish:(SKRequest *)request { - [self removeFromActiveRequests]; -} - -- (void)request:(SKRequest *)request didFailWithError:(NSError *)error { - [[USRVWebViewApp getCurrentApp] sendEvent:@"PRODUCT_REQUEST_FAILED" category:@"STORE" param1:self.requestId, [error description], nil]; - - [self removeFromActiveRequests]; -} - -@end diff --git a/UnityServices/Store/Transactions/USTRTransactionObserver.h b/UnityServices/Store/Transactions/USTRTransactionObserver.h deleted file mode 100644 index ffc404a3..00000000 --- a/UnityServices/Store/Transactions/USTRTransactionObserver.h +++ /dev/null @@ -1,5 +0,0 @@ -#import - -@interface USTRTransactionObserver : NSObject - -@end diff --git a/UnityServices/Store/Transactions/USTRTransactionObserver.m b/UnityServices/Store/Transactions/USTRTransactionObserver.m deleted file mode 100644 index bba70e88..00000000 --- a/UnityServices/Store/Transactions/USTRTransactionObserver.m +++ /dev/null @@ -1,54 +0,0 @@ -#import "USTRTransactionObserver.h" -#import "USRVWebViewApp.h" -#import "USTRStore.h" - -@implementation USTRTransactionObserver - -- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { - NSMutableArray *transactionData = [[NSMutableArray alloc] init]; - - for (SKPaymentTransaction *transaction in transactions) { - SKPayment *payment = transaction.payment; - NSData *receipt = [USTRStore getReceipt]; - NSString *encodedReceipt = [receipt base64EncodedStringWithOptions:0]; - BOOL hasOriginalTransaction = transaction.originalTransaction != nil; - - NSMutableDictionary *productDict = [[NSMutableDictionary alloc] init]; - - if (transaction.payment.productIdentifier) { - [productDict setObject:transaction.payment.productIdentifier forKey:@"productId"]; - } - if (transaction.transactionState) { - [productDict setObject:[NSNumber numberWithInteger:transaction.transactionState] forKey:@"transactionState"]; - } - if (transaction.transactionDate) { - [productDict setObject:[NSNumber numberWithDouble:[transaction.transactionDate timeIntervalSince1970]] forKey:@"transactionDate"]; - } - if (transaction.transactionIdentifier) { - [productDict setObject:transaction.transactionIdentifier forKey:@"transactionId"]; - } - - NSMutableDictionary *paymentDict = [[NSMutableDictionary alloc] init]; - - if (payment.productIdentifier) { - [paymentDict setObject:payment.productIdentifier forKey:@"productId"]; - } - if (payment.quantity) { - [paymentDict setObject:[NSNumber numberWithInteger:payment.quantity] forKey:@"numberOfItems"]; - } - if (paymentDict) { - [productDict setObject:paymentDict forKey:@"payment"]; - } - if (encodedReceipt) { - [productDict setObject:encodedReceipt forKey:@"receipt"]; - } - - [productDict setObject:[NSNumber numberWithBool:hasOriginalTransaction] forKey:@"hasOriginalTransaction"]; - - [transactionData addObject:productDict]; - } - - [[USRVWebViewApp getCurrentApp] sendEvent:@"RECEIVED_TRANSACTION" category:@"STORE" param1:transactionData, nil]; -} - -@end diff --git a/UnityServices/Store/UADSAppStoreReceiptReader.h b/UnityServices/Store/UADSAppStoreReceiptReader.h new file mode 100644 index 00000000..1c95ef1d --- /dev/null +++ b/UnityServices/Store/UADSAppStoreReceiptReader.h @@ -0,0 +1,14 @@ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol UADSAppStoreReceiptReader +-(NSString *)encodedReceipt; +@end + +@interface UADSAppStoreReceiptReaderImp : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/Store/UADSAppStoreReceiptReader.m b/UnityServices/Store/UADSAppStoreReceiptReader.m new file mode 100644 index 00000000..0dcfe111 --- /dev/null +++ b/UnityServices/Store/UADSAppStoreReceiptReader.m @@ -0,0 +1,12 @@ + +#import "UADSAppStoreReceiptReader.h" + +@implementation UADSAppStoreReceiptReaderImp + +- (nonnull NSString *)encodedReceipt { + NSURL *receiptURL = NSBundle.mainBundle.appStoreReceiptURL; + NSData *receipt = [NSData dataWithContentsOfURL:receiptURL]; + return [receipt base64EncodedStringWithOptions: 0]; +} + +@end diff --git a/UnityServices/Store/USTRStore.h b/UnityServices/Store/USTRStore.h index 0da89223..f77b000b 100644 --- a/UnityServices/Store/USTRStore.h +++ b/UnityServices/Store/USTRStore.h @@ -1,11 +1,24 @@ #import "USTRAppSheet.h" +#import "UADSTransactionObserver.h" +#import "UADSAppStoreReceiptReader.h" +#import "UADSSKProductReader.h" -@interface USTRStore : NSObject +typedef void(^USTRStoreProductsCompletion)(NSArray *products); + +@interface USTRStore: NSObject + + ++(instancetype)newWithProductReader: (id) productsReader + andReceiptReader: (id)receiptReader; ++(instancetype)sharedInstance; + +- (void)startTransactionObserverWithCompletion: (UADSTransactionObserverCompletion) completion; +- (void)stopTransactionObserver; +- (void)getProductsUsingIDs: (NSArray*)productIDs + success: (USTRStoreProductsCompletion) completion + onError: (UADSSKProductReaderErrorCompletion) onError; -+ (void)startTransactionObserver; -+ (void)stopTransactionObserver; -+ (void)requestProductInfos:(NSArray*)productIds requestId:(NSNumber *)requestId; + (USTRAppSheet *)appSheet; -+ (NSData*)getReceipt; + @end diff --git a/UnityServices/Store/USTRStore.m b/UnityServices/Store/USTRStore.m index 838741dd..52e6d963 100644 --- a/UnityServices/Store/USTRStore.m +++ b/UnityServices/Store/USTRStore.m @@ -2,14 +2,32 @@ #import #import "USTRStore.h" #import "USRVDevice.h" -#import "USTRTransactionObserver.h" -#import "USTRProductRequest.h" +#import "UADSTools.h" +#import "NSArray + Map.h" +#import "SKProductBridge + Dictionary.h" + +@interface USTRStore() +@property (nonatomic, nonnull, strong) id receiptReader; +@property (nonatomic, nonnull, strong) id productsReader; +@property (nonatomic, strong) UADSTransactionObserver* transactionObserver; +@property (nonatomic, strong) SKPaymentQueue* queue; +@end + @implementation USTRStore -static USTRTransactionObserver *transactionObserver = NULL; static USTRAppSheet *appSheet = NULL; ++ (instancetype)newWithProductReader:(id)productsReader + andReceiptReader:(id)receiptReader { + + USTRStore *obj = [[self alloc] init]; + obj.receiptReader = receiptReader; + obj.queue = SKPaymentQueue.defaultQueue; + obj.productsReader = productsReader; + return obj; +} + + (USTRAppSheet *)appSheet { if (!appSheet) { appSheet = [[USTRAppSheet alloc] init]; @@ -18,29 +36,50 @@ + (USTRAppSheet *)appSheet { return appSheet; } -+ (void)startTransactionObserver { - if (!transactionObserver) { - transactionObserver = [[USTRTransactionObserver alloc] init]; - [[SKPaymentQueue defaultQueue] addTransactionObserver:transactionObserver]; - } ++(instancetype)sharedInstance { + static USTRStore *sharedStoreKit = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + UADSAppStoreReceiptReaderImp *defaultReceiptReader = [UADSAppStoreReceiptReaderImp new]; + UADSSKProductReaderImp *productsReader = [UADSSKProductReaderImp new]; + sharedStoreKit = [USTRStore newWithProductReader: productsReader + andReceiptReader: defaultReceiptReader]; + }); + return sharedStoreKit; } -+ (void)stopTransactionObserver { - if (transactionObserver) { - [[SKPaymentQueue defaultQueue] removeTransactionObserver:transactionObserver]; - transactionObserver = NULL; - } +- (void)startTransactionObserverWithCompletion: (UADSTransactionObserverCompletion) completion { + GUARD(!self.transactionObserver) + + _transactionObserver = [UADSTransactionObserver newWithReceiptReader:_receiptReader + andCompletion: completion]; + [_queue addTransactionObserver: _transactionObserver]; +} + +- (void)stopTransactionObserver { + GUARD(self.transactionObserver) + [_queue removeTransactionObserver: _transactionObserver]; + self.transactionObserver = nil; } -+ (void)requestProductInfos:(NSArray*)productIds requestId:(NSNumber *)requestId { - USTRProductRequest *productRequest = [[USTRProductRequest alloc] initWithProductIds:productIds requestId:requestId]; - [productRequest requestProducts]; +- (void)getProductsUsingIDs: (NSArray *)productIDs + success: (USTRStoreProductsCompletion)completion + onError: (UADSSKProductReaderErrorCompletion)onError { + + [_productsReader fetchProductsUsingIDS: productIDs + completion: ^(NSArray * _Nonnull products) { + + NSArray *arrayOfDictionaries = [products uads_mapObjectsUsingBlock:^id _Nonnull(SKProduct * _Nonnull obj) { + return [SKProductBridge getProxyWithObject: obj].uads_Dictionary; + }]; + completion(arrayOfDictionaries); + + } onErrorCompletion:onError]; } -+ (NSData*)getReceipt { - NSURL *receiptURL = [NSBundle bundleForClass:[self class]].appStoreReceiptURL; - NSData *receipt = [NSData dataWithContentsOfURL:receiptURL]; - return receipt; +- (NSString *)encodedReceipt { + return _receiptReader.encodedReceipt; } + @end diff --git a/UnityServices/UADSTools/Categories/NSArray/NSArray + Map.h b/UnityServices/UADSTools/Categories/NSArray/NSArray + Map.h new file mode 100644 index 00000000..b9b10b6d --- /dev/null +++ b/UnityServices/UADSTools/Categories/NSArray/NSArray + Map.h @@ -0,0 +1,11 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef id _Nonnull (^NSArrayMapBlock)(id obj); + +@interface NSArray(Map) +- (NSArray *)uads_mapObjectsUsingBlock:(NSArrayMapBlock)block; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/UADSTools/Categories/NSArray/NSArray + Map.m b/UnityServices/UADSTools/Categories/NSArray/NSArray + Map.m new file mode 100644 index 00000000..2549ab33 --- /dev/null +++ b/UnityServices/UADSTools/Categories/NSArray/NSArray + Map.m @@ -0,0 +1,11 @@ +#import "NSArray + Map.h" + +@implementation NSArray(Map) +- (NSArray *)uads_mapObjectsUsingBlock:(NSArrayMapBlock)block { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:[self count]]; + [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + [result addObject:block(obj)]; + }]; + return result; +} +@end diff --git a/UnityServices/UADSTools/Categories/NSArray/NSArray+Convenience.h b/UnityServices/UADSTools/Categories/NSArray/NSArray+Convenience.h new file mode 100644 index 00000000..bebc2c64 --- /dev/null +++ b/UnityServices/UADSTools/Categories/NSArray/NSArray+Convenience.h @@ -0,0 +1,11 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSArray(Category) + +- (bool)uads_allSatisfy:(bool (^)(ObjectType obj))block; + +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/UADSTools/Categories/NSArray/NSArray+Convenience.m b/UnityServices/UADSTools/Categories/NSArray/NSArray+Convenience.m new file mode 100644 index 00000000..63f679d3 --- /dev/null +++ b/UnityServices/UADSTools/Categories/NSArray/NSArray+Convenience.m @@ -0,0 +1,16 @@ +#import "NSArray+Convenience.h" + +@implementation NSArray(Convenience) +- (bool)uads_allSatisfy:(bool (^)(id _Nonnull))block { + __block bool result = true; + [self enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (!block(obj)) { + result = false; + *stop = YES; + + } + }]; + + return result; +} +@end diff --git a/UnityServices/UADSTools/Categories/NSDate/NSDate + NSNumber.h b/UnityServices/UADSTools/Categories/NSDate/NSDate + NSNumber.h new file mode 100644 index 00000000..7d8af180 --- /dev/null +++ b/UnityServices/UADSTools/Categories/NSDate/NSDate + NSNumber.h @@ -0,0 +1,10 @@ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSDate(NSNumber) +- (NSNumber *)uads_timeIntervalSince1970; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/UADSTools/Categories/NSDate/NSDate + NSNumber.m b/UnityServices/UADSTools/Categories/NSDate/NSDate + NSNumber.m new file mode 100644 index 00000000..8e9b3f3c --- /dev/null +++ b/UnityServices/UADSTools/Categories/NSDate/NSDate + NSNumber.m @@ -0,0 +1,7 @@ +#import "NSDate + NSNumber.h" + +@implementation NSDate(NSNumber) +- (NSNumber *)uads_timeIntervalSince1970 { + return [NSNumber numberWithDouble: self.timeIntervalSince1970]; +} +@end diff --git a/UnityServices/UADSTools/Categories/NSInvocation/NSEnumWrapper/NSPrimitivesBox.h b/UnityServices/UADSTools/Categories/NSInvocation/NSEnumWrapper/NSPrimitivesBox.h new file mode 100644 index 00000000..756098b5 --- /dev/null +++ b/UnityServices/UADSTools/Categories/NSInvocation/NSEnumWrapper/NSPrimitivesBox.h @@ -0,0 +1,18 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + The class is used in invocation when we want to pass any primitive values to a function with NSInvocation + + @discussion + The problem: NSInvocation category accepts arguments as NSArray that operates with id. We could have used NSValue for those purposes but + in this case we would loose the ability to operate with NSNumbers. NSNumber is subclass of NSValue and since NSInvocation uses typecast to a box + this creates the problem. For that reason NSPrimitivesBox provides the solution by expecting any primitives for a function been wrapped into it + + */ +@interface NSPrimitivesBox: NSValue ++(instancetype)newWithBytes:(nonnull const void *) bytes objCType:(nonnull const char *) type; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/UADSTools/Categories/NSInvocation/NSEnumWrapper/NSPrimitivesBox.m b/UnityServices/UADSTools/Categories/NSInvocation/NSEnumWrapper/NSPrimitivesBox.m new file mode 100644 index 00000000..be97c9bc --- /dev/null +++ b/UnityServices/UADSTools/Categories/NSInvocation/NSEnumWrapper/NSPrimitivesBox.m @@ -0,0 +1,36 @@ +#import "NSPrimitivesBox.h" +#import "UADSTools.h" + +@interface NSPrimitivesBox() +@property (nonatomic, strong) NSValue *box; +@end + +@implementation NSPrimitivesBox + ++ (instancetype)newWithBytes:(nonnull const void *) bytes objCType:(nonnull const char *) type { + return [[self alloc] initWithBytes: bytes objCType: type]; +} + +// Suppressing unnecessary warnings. Its expected behaviour when subclassing NSValue, according to the notes from apple docs +// https://developer.apple.com/documentation/foundation/nsvalue?language=objc +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wobjc-designated-initializers" +- (instancetype)initWithBytes:(const void *)value objCType:(const char *)type { +#pragma GCC diagnostic pop + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wobjc-designated-initializers" + SUPER_INIT +#pragma GCC diagnostic push + self.box = [NSValue valueWithBytes:value objCType:type]; + return self; +} + +- (void)getValue:(void *)value { + return [_box getValue: value]; +} + +- (const char *)objCType { + return [_box objCType]; +} +@end diff --git a/UnityServices/UADSTools/Categories/NSInvocation/NSInvocation+Convenience.h b/UnityServices/UADSTools/Categories/NSInvocation/NSInvocation+Convenience.h new file mode 100644 index 00000000..bcbca9ab --- /dev/null +++ b/UnityServices/UADSTools/Categories/NSInvocation/NSInvocation+Convenience.h @@ -0,0 +1,58 @@ +NS_ASSUME_NONNULL_BEGIN + + + +/* + Category that provides convenience static methods to create and perform invocations. + + */ +@interface NSInvocation(Category) + + +/// Invokes a method on a Class or on an instance with a set of arguments +/// @note If a target is nil, will try to perform static method on a class. +/// @param methodName Full name of a selector +/// @param classType Class +/// @param target target of type Class +/// @param arguments arguments required by the a function +/// +/// @note When we need to pass an enum as an argument, use NSEnumWrapper +/// @code NSEnumWrapper *typeWrapped = [NSEnumWrapper newWithBytes:&`EnumValue` objCType: @encode(EnumType)]; ++ (void)uads_invokeUsingMethod: (NSString *)methodName + classType: (Class)classType + target: (_Nullable id) target + args: (NSArray *)arguments; + + +/// Creates a prepared NSInvocation to perform a method of a Class or on an instance with a set of arguments +/// @note If a target is nil, will try to perform static method on a class. +/// @param methodName Full name of a selector +/// @param classType Class +/// @param target target of type Class +/// @param arguments arguments required by the a function +/// +/// @note When we need to pass an enum as an argument, use NSEnumWrapper +/// @code NSEnumWrapper *typeWrapped = [NSEnumWrapper newWithBytes:&`EnumValue` objCType: @encode(EnumType)]; ++(nullable instancetype)uads_newUsingMethod: (NSString *)methodName + classType: (Class) classType + target: (_Nullable id) target + args: (NSArray *) arguments; + + +/// Invokes a method on a Class or on an instance with a set of arguments. Returns a result of invoked function +/// @note If a target is nil, will try to perform static method on a class. +/// +/// @param methodName Full name of a selector +/// @param classType Class +/// @param target target of type Class +/// @param arguments arguments required by the a function +/// +/// @note When we need to pass an enum as an argument, use NSEnumWrapper +/// @code NSEnumWrapper *typeWrapped = [NSEnumWrapper newWithBytes:&`EnumValue` objCType: @encode(EnumType)]; ++ (nullable id)uads_invokeWithReturnedUsingMethod: (NSString *)methodName + classType: (Class)classType + target: (_Nullable id) target + args: (NSArray *)arguments; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/UADSTools/Categories/NSInvocation/NSInvocation+Convenience.m b/UnityServices/UADSTools/Categories/NSInvocation/NSInvocation+Convenience.m new file mode 100644 index 00000000..0a5bce2f --- /dev/null +++ b/UnityServices/UADSTools/Categories/NSInvocation/NSInvocation+Convenience.m @@ -0,0 +1,79 @@ +#import "NSInvocation+Convenience.h" +#import "NSPrimitivesBox.h" +#import "UADSTools.h" + +@implementation NSInvocation(Category) + ++ (void)uads_invokeUsingMethod: (NSString *)methodName + classType: (Class)classType + target: (_Nullable id) target + args: (NSArray *)arguments { + NSInvocation* invocation = [self uads_newUsingMethod: methodName + classType: classType + target: target + args: arguments]; + [invocation invoke]; +} + ++ (nullable id)uads_invokeWithReturnedUsingMethod: (NSString *)methodName + classType: (Class)classType + target: (_Nullable id) target + args: (NSArray *)arguments { + __autoreleasing id returnedValue; + NSInvocation* invocation = [self uads_newUsingMethod: methodName + classType: classType + target: target + args: arguments]; + [invocation invoke]; + [invocation getReturnValue: &returnedValue]; + return returnedValue; +} + ++(nullable instancetype)uads_newUsingMethod: (NSString *)methodName + classType: (Class) classType + target: (_Nullable id) target + args: (NSArray *) arguments { + SEL selector = NSSelectorFromString(methodName); + NULL_CHECK_OR_RETURN_NIL(selector) + + __autoreleasing id targetArg; + NSMethodSignature *signature; + if (!target) { + targetArg = classType; + signature = [classType methodSignatureForSelector: selector]; + } else { + targetArg = target; + signature = [classType instanceMethodSignatureForSelector: selector]; + } + NULL_CHECK_OR_RETURN_NIL(signature) + + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature: signature]; + [invocation setSelector: selector]; + [invocation setTarget: targetArg]; + for (int i = 0; i < [arguments count]; i++) { + __autoreleasing id argument = arguments[i]; + NSPrimitivesBox *value = TYPECAST(argument, [NSPrimitivesBox class]); + + if (value) { + void *pointerToPrimitives; + [value getValue: &pointerToPrimitives]; + + /** from https://developer.apple.com/documentation/foundation/nsinvocation/1437834-setargument + An integer specifying the index of the argument. + Indices 0 and 1 indicate the hidden arguments self and _cmd, respectively; you should set these values directly with the target and selector properties. Use indices 2 and greater for the arguments normally passed in a message. + */ + + [invocation setArgument: &pointerToPrimitives atIndex: 2 + i]; + } else { + [invocation setArgument: &argument atIndex: 2 + i]; + } + } + + if (!invocation.argumentsRetained) { + [invocation retainArguments]; + } + + return invocation; +} + +@end diff --git a/UnityServices/UADSTools/Categories/NSMutableDictionary/NSMutableDictionary + SafeOperations/NSMutableDictionary + SafeOperations.h b/UnityServices/UADSTools/Categories/NSMutableDictionary/NSMutableDictionary + SafeOperations/NSMutableDictionary + SafeOperations.h new file mode 100644 index 00000000..3bcf6d14 --- /dev/null +++ b/UnityServices/UADSTools/Categories/NSMutableDictionary/NSMutableDictionary + SafeOperations/NSMutableDictionary + SafeOperations.h @@ -0,0 +1,7 @@ +NS_ASSUME_NONNULL_BEGIN + +@interface NSMutableDictionary(SafeOperations) +-(void)uads_setValueIfNotNil: (nullable id)object forKey: (nonnull NSString *) key; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/UADSTools/Categories/NSMutableDictionary/NSMutableDictionary + SafeOperations/NSMutableDictionary + SafeOperations.m b/UnityServices/UADSTools/Categories/NSMutableDictionary/NSMutableDictionary + SafeOperations/NSMutableDictionary + SafeOperations.m new file mode 100644 index 00000000..5b2690d4 --- /dev/null +++ b/UnityServices/UADSTools/Categories/NSMutableDictionary/NSMutableDictionary + SafeOperations/NSMutableDictionary + SafeOperations.m @@ -0,0 +1,10 @@ +#import "NSMutableDictionary + SafeOperations.h" +#import "UADSTools.h" + +@implementation NSMutableDictionary(SafeOperations) +- (void)uads_setValueIfNotNil:(id)object forKey:(NSString *)key { + GUARD(object) + [self setValue: object forKey: key]; +} + +@end diff --git a/UnityServices/UADSTools/Categories/NSObject/NSObject+Convenience.h b/UnityServices/UADSTools/Categories/NSObject/NSObject+Convenience.h new file mode 100644 index 00000000..e33936a1 --- /dev/null +++ b/UnityServices/UADSTools/Categories/NSObject/NSObject+Convenience.h @@ -0,0 +1,9 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSObject(Category) ++ (bool)containsMethods: (NSArray*)names; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/UADSTools/Categories/NSObject/NSObject+Convenience.m b/UnityServices/UADSTools/Categories/NSObject/NSObject+Convenience.m new file mode 100644 index 00000000..32efe961 --- /dev/null +++ b/UnityServices/UADSTools/Categories/NSObject/NSObject+Convenience.m @@ -0,0 +1,30 @@ +#import "NSArray+Convenience.h" +#import "NSObject+Convenience.h" + +@implementation NSObject(Category) + ++ (bool)containsMethods: (NSArray*)names { + if (names.count == 0) { + return true; + } + + return [names uads_allSatisfy: ^bool(NSString * _Nonnull obj) { + return [self containsMethod: obj]; + }]; +} + ++ (bool)containsMethod:(NSString *) name { + bool result = false; + SEL selector = NSSelectorFromString(name); + if (!selector) { + return result; + } + + result = ([self methodSignatureForSelector: selector] != nil || + [self instanceMethodSignatureForSelector: selector] != nil); + return result; +} + + + +@end diff --git a/UnityServices/UADSTools/UADSProxyReflection/UADSProxyReflection.h b/UnityServices/UADSTools/UADSProxyReflection/UADSProxyReflection.h new file mode 100644 index 00000000..96b45452 --- /dev/null +++ b/UnityServices/UADSTools/UADSProxyReflection/UADSProxyReflection.h @@ -0,0 +1,125 @@ +NS_ASSUME_NONNULL_BEGIN + +/** + Class-proxy that wraps any object that needs to be initialized or called method on it reflectively + + Provides the next functionality + + - Init any object reflectively using instance or class methods. + + - Check if an object exists. + + - Check if an object is valid/was initialized properly. Should be done by subclasses. + + - Unified method to call any instance function reflectively. + + @code + // assuming we have ObjectA and ObjectB with the + // next declarations that need to be called reflectively + + @interface ObjectA + -(instancetype)initWithID: (NSString *)id; + -(NSDictionary *)source; + -(NSString *)requestID; + @end + + @interface ObjectB + +(instancetype)createUsingObject: (ObjectA *)obj + @end + + //Declare and implement Bridges + @interface ObjectABridge + +(instancetype)newWithID: (NSString *)id; + @end + + @implementation ObjectABridge() + +(NSString *)className { + return @"ObjectA"; + } + + +(instancetype)newWithID: (NSString *)id { + return [self getInstanceUsingMethod:@"initWithID:" + args:@[id]]; + } + @end + + @interface ObjectBBridge + +(instancetype)createUsingObject: (ObjectABridge *)obj + @end + + @implementation ObjectBBridge + +(NSString *)className { + return @"ObjectB"; + } + + +(instancetype)createUsingObject: (ObjectABridge *)obj { + return [self getInstanceUsingClassMethod:@"createUsingObject:" + args: @[]]; + } + @end + + // Now you can use them as a strong-type objects, intercept the calls if need. + if (![ObjectABridge exists]) { + // quit from the function with error if need + return nil; + } + ObjectABridge *objA = [ObjectABridge newWithID: @"ID"]; + ObjectBBridge *objB = [ObjectBBridge createUsingObject: objA]; + */ + +@interface UADSProxyReflection: NSObject +@property (nonatomic, strong,readonly) NSObject *proxyObject; + +/// A subclass should provide a class name of an object to work with reflectivly. ++(NSString *)className; + +/// Returns a class of an object ++(Class)getClass; + +/// Returns if the object with `className` exists ++(bool)exists; + +/// Create an object using instance method +/// @param methodName Instance method initializer +/// @param arguments An Array of arguments for initializer ++(instancetype)getInstanceUsingMethod:(NSString *)methodName + args:(NSArray *)arguments; + +/// Create an object using class method +/// @param methodName Class method for initializing/creating an object +/// @param arguments An Array of arguments for initializer ++(instancetype)getInstanceUsingClassMethod:(NSString *)methodName + args:(NSArray *)arguments; + +/// Create a proxy with an object +/// @param object Any object ++(instancetype)getProxyWithObject:(id)object; + +/// Initialize a proxy with an object +/// @param object Any object +-(instancetype)initWithProxyObject:(_Nullable id)object; + +/// Returns if an object is valid. By default it checks if the wrapped object is not nil +-(bool)isValid; + +/// Call an instance method reflectively +/// @param methodName Instance method name. Should be a format: @"methodname:arg1:arg2" +/// @param arguments An Array of arguments for the method +-(void)callInstanceMethod: (NSString *)methodName + args: (NSArray *)arguments; + + + +// Subclasses can override it to provide extended check for class existance +// Include the selectors that are required for a class to operate ++ (NSArray *)requiredSelectors; + + +// Subclasses can override it to provide extended check for class existance +// Include keys that are used by KVC and are required for a class to operate ++ (NSArray *)requiredKeysForKVO; + + +@end + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/UADSTools/UADSProxyReflection/UADSProxyReflection.m b/UnityServices/UADSTools/UADSProxyReflection/UADSProxyReflection.m new file mode 100644 index 00000000..23f45433 --- /dev/null +++ b/UnityServices/UADSTools/UADSProxyReflection/UADSProxyReflection.m @@ -0,0 +1,177 @@ +#import "UADSProxyReflection.h" +#import "NSInvocation+Convenience.h" +#import "NSArray+Convenience.h" +#import "NSObject+Convenience.h" + +@interface UADSProxyReflection() +@property (nonatomic, strong) NSObject *proxyObject; +@end + +@implementation UADSProxyReflection + ++ (NSString *)className { + return @""; +} + ++ (NSArray *)requiredSelectors { + return @[]; +} + ++ (NSArray *)requiredKeysForKVO { + return @[]; +} + ++ (bool)exists { + return [self getClass] != nil && + [self respondToRequiredSelectors] && + [self respondToRequiredKeys]; +} + ++ (bool)respondToRequiredSelectors { + return [[self getClass] containsMethods: self.requiredSelectors]; +} + ++ (bool)respondToRequiredKeys { + return [[self getClass] containsMethods: self.requiredKeysForKVO]; +} + ++ (instancetype)getProxyWithObject:(id)object { + return [[self alloc] initWithProxyObject: object]; +} + ++ (instancetype)getInstanceUsingMethod:(NSString *)methodName + args:(NSArray *)arguments { + return [self getInstanceUsingMethod: methodName + classMethod: false + args: arguments]; +} + ++ (instancetype)getInstanceUsingClassMethod:(NSString *)methodName + args:(NSArray *)arguments { + return [self getInstanceUsingMethod: methodName + classMethod: true + args: arguments]; +} + ++ (Class)getClass { + return NSClassFromString([self className]); +} + ++ (instancetype)getInstanceUsingMethod: (NSString *)methodName + classMethod: (bool) isClassMethod + args: (NSArray *)arguments { + Class class = [self getClass]; + NULL_CHECK_OR_RETURN_NIL(class) + + SEL selector = NSSelectorFromString(methodName); + NULL_CHECK_OR_RETURN_NIL(selector) + + __autoreleasing id obj; + if (isClassMethod) { + obj = [NSInvocation uads_invokeWithReturnedUsingMethod: methodName + classType: class + target: nil + args: arguments]; + } else { + obj = [class alloc]; + NULL_CHECK_OR_RETURN_NIL(obj) + [NSInvocation uads_invokeUsingMethod: methodName + classType: class + target: obj + args: arguments]; + } + + return [[self alloc] initWithProxyObject: obj]; +} + +- (bool)isValid { + return _proxyObject != nil; +} + +- (instancetype)initWithProxyObject:(_Nullable id)object{ + self.proxyObject = object; + return self; +} + +- (NSObject *)proxyObject { + return _proxyObject; +} + +- (void)callInstanceMethod: (NSString *)methodName + args: (NSArray *)arguments { + [NSInvocation uads_invokeUsingMethod: methodName + classType: [[self class] getClass] + target: self.proxyObject + args: arguments]; +} + +- (id)forwardingTargetForSelector:(SEL)aSelector { + if ([self.proxyObject respondsToSelector:aSelector]) { + return self.proxyObject; + } + return nil; + } + + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { + NSMethodSignature *methodSignature; + methodSignature = [self.proxyObject methodSignatureForSelector: aSelector]; + if (!methodSignature) { + methodSignature = self.fallbackSignature; + } + return methodSignature; + } + + /** If the proxyObject is nil we need to provide placeholder for the method signature to avoid crash of type "[NSProxy doesNotRecognizeSelector... + From the docs https://developer.apple.com/documentation/foundation/nsmethodsignature + + A method signature consists of one or more characters for the method return type, + followed by the string encodings of the implicit arguments self and _cmd, followed by zero or more explicit arguments. + + You can determine the string encoding and the length of a return type using methodReturnType and methodReturnLength properties. + You can access arguments individually using the getArgumentTypeAtIndex: method and numberOfArguments property. + + So the idea basically to create a string that contains necessary arguments and then using `signatureWithObjCTypes` create a "void" signature to prevent crash. + + */ +- (NSMethodSignature *)fallbackSignature { + NSString *firstArgument = [NSString stringWithUTF8String:@encode(id)]; + NSString *secondArgument = [NSString stringWithUTF8String:@encode(SEL)]; + NSString *types = [NSString stringWithFormat:@"%@%@", firstArgument, secondArgument]; + const char *objCTypes = [types UTF8String]; + if (objCTypes) { + return [NSMethodSignature signatureWithObjCTypes: objCTypes]; + } else { + return nil; + } + } + +- (void)forwardInvocation:(NSInvocation *)anInvocation { + + if (anInvocation.methodSignature.numberOfArguments < 2) { + // invalid signature, we should skip it + // Indication to fallBackSignature: + // There are always at least two arguments, because an NSMethodSignature + // object includes the implicit arguments self and _cmd, + // which are the first two arguments passed to every method implementation. + return; + } + + [anInvocation invokeWithTarget: self.proxyObject]; +} + +- (id)valueForKey:(NSString *)key { + if ([self.proxyObject respondsToSelector: NSSelectorFromString(key)]) { + return [self.proxyObject valueForKey:key]; + } else { + return [super valueForKey: key]; + } +} + +- (id)valueForUndefinedKey:(NSString *)key { + return nil; +} + +@end + + diff --git a/UnityServices/UADSTools/UADSTools.h b/UnityServices/UADSTools/UADSTools.h new file mode 100644 index 00000000..3499aef9 --- /dev/null +++ b/UnityServices/UADSTools/UADSTools.h @@ -0,0 +1,36 @@ + +NS_ASSUME_NONNULL_BEGIN + +/** + Macro that replaces boilerplate code like: + @code + + if ([obj isKindOfClass: class]) { + return obj; + } else { + return nil; + } + */ +#define TYPECAST(obj, class) typecast(obj, class) + + +/** + Macro that replaces boilerplate code for nil checking and returning from the scope of a function + */ +#define NULL_CHECK_OR_RETURN_NIL(obj) if (!obj) { return nil;} + + + +/** + Macro that replaces boilerplate code when calling super init + */ +#define SUPER_INIT self = [super init]; if (!self) { return nil ;} + + + + +#define GUARD(condition) if (!condition) { return; } + +_Nullable id typecast(id obj, Class class); + +NS_ASSUME_NONNULL_END diff --git a/UnityServices/UADSTools/UADSTools.m b/UnityServices/UADSTools/UADSTools.m new file mode 100644 index 00000000..19afb5bd --- /dev/null +++ b/UnityServices/UADSTools/UADSTools.m @@ -0,0 +1,11 @@ +#import "UADSTools.h" + + +_Nullable id typecast(id obj, Class class) { + if ([obj isKindOfClass: class]) { + return obj; + } else { + return nil; + } +} +