iOS2018.08.04 13:17


애플의 인앱결재의 자동결재를 정리한다.


종류는 다음과 같다.


  • 소비형 : 한번 사면 바로 소모됨
  • 비소비형 : 한번사면 평생 유효
  • 자동갱신형 : 주기적으로 재구매(결재)가 자동으로 이루어짐
  • 비갱신형 : 기간 만료되면 다시 구매를 해야 함.

자동갱신

애플에서는 해당 구매의 만료일 10일전에 결재인프라를 먼저 확인한다. 이 때 카드정보등이 유효하지 않으면 메일등을 보내서 갱신을 유도한다.

하루전에는 실제로 결재를 시도한다. 이 때 결재가 실패할 수 있다. 

두가지가 있을 수 있다.


  • 결제정보의 일시적인 오류
  • 정기결제기능을 off

위 상황에서는 결재가 실패하게 되며 사용자가 결재정보를 유효하게 갱신하거나 정기결재기능을 다시 on하게 되면 애플에서는 구매를 수행하게 된다. 이 때 부득이 지연이 발생한다.


앱을 서비스 하는 입장에서는 이런 지연이 발생 한경우를 파악하여 적절히 사용자에게 안내를 해야 불온한 의도를 가진 사용자들의 공격을 막아낼 수 있을 것이다.


재결제내역 확인

앱과 서버가 모두 수행하는것이 좋으며 일반적으로 서버에서 하는것이 좀더 안전하다. 왜 더 안전한지에 대한 설명은 찾아볼 수 없으나 예상으로는 앱은 해킹이 가능하기 때문으로 추정해본다. 


앱 기동시에는 addTransactionObserver를 호출하면 된다. 이 때 재구매건이 있으면 paymentQueue에 결제건이 들어오게 된다. 이 결재건을 사용자가 직접 구매했을 때 와 동일하게 처리하면 된다.


서버에서는 만료일이 가까워진 결재건에 대해서 original 결재정보로 apple서버에 조회하여 마지막 영수증정보과 최종 영수증 정보가 다르면 리뉴얼 된것으로 파악하면 된다.


영수증정보의 갱신

영수증정보는 동일계정으로 로그인 된 다른 단말에서 재구매가 일어나도 앱 기동시마다 영수증정보를 최신으로 유지하기 때문에 동기화가 가능하다.


현재 구매상태인지 여부 확인

최초 결제의 영수증의 transactionId가 originalTransactionId가 되며 이 후 재결제가 일어나면 새로운 영수증이 발급되며 이 때 transactionId도 갱신이 된다. 하지만 originalTransactionId는 최초의 값이 유지된다.

따라서 영수증 목록에서 originalTransactionId로 필터링 후 expireDate가 아직 미래인 영수증이 있다면 아직 구매중인것으로 판단하면 된다.


구매취소 여부 확인

영수증에 Cancelation Date가 있다면 구독취소한것으로 판단하면 된다고 한다. Cancelation필드가 있으면 ExpireDate와 관계없이 취소된 구독이다.



결재내역 복원하기





Posted by 삼스
iOS2018.02.13 15:51


iOS7이전에는 자바스크립트와 무언가 작용을 하려면 UIWebView의 stringByEvaluatingJavaScriptFormString:를 사용해야 했다. 이는 웹뷰의 자바스크립트 런타임에서만 가능했다. 이는 싱글스레드인 환경의 제약이 있다.


JavaScriptCore는 Objective-C에서 자바스크립트의 풀런타임환경에 접근이 가능하게 한다. 문법확인, 스크립트실행과 변수 접근, 콜백수신  그리고 Objective-C 객체의 공유등으로 광범위한 상호작용이 가능하다.


JSVirtualMachine클래스로 불리는 가상머신에서 실행된다. 이는 여러개의 VM을 통해 멀티스레드 자바스크립팅이 가능하다는 것이다. JSVirtualMachine 각각은 여러개의 JSContext를 가질 수 있다. JSContext는 자바스크립트 런타입환경에 말하고 몇가지 기능을 제공한다. 이 중 두가지는 글로벌객체에 접근하는것과 스크립트를 실행할 수 있다는 것이다.


글로벌 영역에 정의된 변수에 대한 접근은 키로 바로 접근이 가능하다. 


    JSContext *context = [[JSContext alloc] initWithVirtualMachine:[[JSVirtualMachine alloc] init]];
    context[@"a"] = @5;
    JSValue *aValue = context[@"a"];
    double a = [aValue toDouble];
    NSLog(@"%.0f", a);

함수호출은 다음과 같다


[context evaluateScript:@”var square = function(x) {return x*x;}”]; 

JSValue *squareFunction = context[@”square”]; 

NSLog(@”%@”, squareFunction); 

JSValue *aSquared = [squareFunction callWithArguments:@[context[@”a”]]]; 

NSLog(@”a^2: %@”, aSquared); 

JSValue *nineSquared = [squareFunction callWithArguments:@[@9]]; 

NSLog(@”9^2: %@”, nineSquared);


evaluateScript로 스크립트 코드를 로드하고 callWithArguments로 스크립트내의 함수를 호출한다. 호출된 결과는 JSValue로 반환된다.

스크립트코드는 블럭코드형태로도 가능하다.



    context[@"factorial"] = ^(int x) {
        int factorial = 1;
        for (; x > 1; x--) {
            factorial *= x;
        }
        return factorial;
    };
    [context evaluateScript:@"var fiveFactorial = factorial(5);"];
    JSValue *fiveFactorial = context[@"fiveFactorial"];
    NSLog(@"5! = %@", fiveFactorial);
스크립트와 objective c간의 타입관계는 다음과 같다.

//   Objective-C type  |   JavaScript type
// --------------------+---------------------
//         nil         |     undefined
//        NSNull       |        null
//       NSString      |       string
//       NSNumber      |   number, boolean
//     NSDictionary    |   Object object
//       NSArray       |    Array object
//        NSDate       |     Date object
//       NSBlock       |   Function object
//          id         |   Wrapper object
//        Class        | Constructor object
스크립트NSNumber와 NSBlocks 타입은 mutable이 아니다 즉 한쪽에서 변경했다고 다른쪽에서 반영이 되지 않는다. id와 Class는 mutable이다. 
임의 오브젝트나 클래스가 자바스크립트에 브릿지될 수 있다. 
NSNull, NSString, NSNumber, NSDictionary, NSArray 또는 NSDate는 관련 클래스 계층을 JavaScript 실행 컨텍스트로 가져 와서 동등한 클래스 및 프로토 타입을 생성하는 JavaScriptCore를 생성하려고 한다.

JSExport protocol로 자바스크립트에 커스텀클래스의 일부를 노출할 수 있다. 래퍼객체는 다른곳에 생성될것이다. 그리하여 한개의 객체가 양쪽의 실행컨텍스트에 공유될 수 있다.

다음이 그 예이다.

    @protocol ThingJSExports <JSExport>
    @property (nonatomic, copy) NSString *name;
    @end

    @interface Thing : NSObject <ThingJSExports>
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic) NSInteger number;
    @end

    @implementation Thing
    - (NSString *)description {
        return [NSString stringWithFormat:@"%@: %d", self.name, self.number];
    }
    @end

JSExport를 상속하여 ThingJSExports를 선언함으로써 자바스크립트에 노출할 수 있다. 이제 objective-c객체의 정보를 변경하면 자바스크립트에 그 값이 반영이 될것이다. 그 반대로 마찬가지.
다음은 id와 class를 브릿지한 예이다.


다음은 id와 class를 브릿지한 예이다.    Thing *thing = [[Thing alloc] init];
    thing.name = @"Alfred";
    thing.number = 3;
    context[@"thing"] = thing;
    JSValue *thingValue = context[@"thing"];

context[@"thing"] = [Thing class];

NSString *thingScript =

@"var t = new thing();"

"t.name = 'yslee';"

"t.number = 10;"

[context eveluateScript:thingScript];








Posted by 삼스
iOS2018.02.13 15:01


iOS에도 이런게 나왔을 줄이야..


안드로이드 하이브리드 개발자들은 알고 있겠지만 addJavascriptInterface를 통해서 자바스크립트에 java object를 바인딩이 가능하다.

이를 통해 하이브리드를 통해서 동기화 호출도 가능하다.

하지만 iOS는 불가했었다.

cordova도 NSURLProtocol을 통해서 하이브리드를 지원하는데 역시 동기화 호출을 불가하다.

하지만 불가능하지는 않다. 다른 방법으로 가능한 방법이 있으며 이를 통해서 실제 동기화 호출을 구현하기도 하였다.


그런데 iOS7부터 지원하는 JavaScriptCore프레임웍으로 가능한것 같다.


http://www.dbotha.com/2014/11/19/using-javascript-core-for-game-scripting-on-ios/ 를 참조하였다.


게임스크립팅 엔진을 javascriptcore로 변경하는것에 대한 글이다.


일반적인 게임튜터리얼의 스크립트는 아래와 같다.


/* tutorial1.js */
showText("Hello World!", /*x:*/ 160, /*y:*/ 100);
wait(5); // sleep 5 seconds
hideText()

showText("Tap the screen to continue", 160, 100);
waitForTap(); // pause execution until the user taps the UI
hideText();

showText("Match three coloured blocks to win!", 160, 100);
waitForLevelEnd(); // pause execution until a level end event occurs
hideText();

loadLevel("level2.map")

예제에서 보듯이 어떤 레벨에 도달할때까지 스크립트실행을 중지하는것이 좋다. 위에서 wait()함수에 대해 의문을 품게 될것이다. 싱글스레드의 자바스크립트 엔진에서는 의도한대로 작동을 기대하기 어렵다. 완전히 스레드가 멈추게 될것이므로..

JSVirtualMachine은 어떻게 실행을 중단할까?


스크립팅엔진자체에 대해 먼저 더 알아보자.


자바스크립트에 접근가능하도록 작성된 스위프트 프로토콜 메서드들을 보자


/* ScriptEngine.swift */
@objc protocol ScriptingEngineExports : JSExport {
    func wait(duration: Double)
    func waitForTap()
    func waitForLevelEnd()
    func loadLevel(mapName: String)
    func showText(text: String, _ x: Double, _ y: Double)
    func hideText()
}


금방 구현가능하고 앞서 기술한 튜터리얼 스크립트에 매우 가깝게 매칭된다.


이번엔 스크립팅 엔진을 살펴보자.


/* ScriptEngine.swift */
@objc class ScriptEngine : NSObject, ScriptEngineExports {
    var context: JSContext!
    let engineQueue = dispatch_queue_create(
            "script_engine", DISPATCH_QUEUE_SERIAL)
    
    override init() {
        super.init()
        
        dispatch_async(engineQueue) {
            self.context = JSContext()
            self.context.setObject(self, forKeyedSubscript: "$")
            self.context.evaluateScript(
                "function wait(duration) {$.wait(duration);}" + 
                "function waitForTap() {$.waitForTap();}" +
                "function waitForLevelEnd(){$.waitForLevelEnd();}" +
                "function print(text) {$.print(text);}" + 
                "function println(text) {$.print(text);}" + 
                "function loadLevel(name) {$.loadLevel(name);}")
        }
    }
    
    /* to be continued... */ 
}

engineQueue은 JSContext를 사용하기 위한 dispatch_queue이다. 이는 메인스레드와 분리하여 스크립트의 콜백을 스위프트가 받을 수 있도록 한다.

여기서 중요한것은 JSContext가 메인스레드에서 분리되어서 인스턴스화되었다는 것이다. 이는 메인스레드가 블럭되는것을 방지한다.


ScriptEngine 객체 인스턴스는 $변수를 통해서 접근이 가능하다.  evaluateScript가 호출되어 스크립트에 억세스 할 수 있는 ScriptEngineExports의 기능을 노출한다. 스위프트의 함수를 호출함으로 인해 ScriptEngine의 메서드가 호출된다.


실행중인 스크립트를 로드하는것은 간단하다. 



/* ScriptEngine.swift */
var currentScriptName: String?

func runScript(scriptName: String) {
    var ext = scriptName.pathExtension
    var fileName = scriptName.substringToIndex(
        advance(scriptName.startIndex, 
                scriptName.utf16Count - ext.utf16Count - 1))
    
    if fileExtension.utf16Count == 0 {
        fileName = scriptName
        ext = "js"
    }
    
    let url = NSBundle.mainBundle()
        .URLForResource(fileName, withExtension: ext)
    let scriptCode = String(contentsOfURL: url!, 
                            encoding: NSUTF8StringEncoding, 
                            error: nil)!

    self.currentScriptName = fileName + "." + fileExtension

    dispatch_async(engineQueue) {
        self.context.evaluateScript(scriptCode)
        return
    }
}

다음과 같이 실행이 가능하다.


let scriptEngine = ScriptEngine()
scriptEngine.runScript("tutorial1")

이제 어떻게 다양한 Wait()함수를 실행할 수 있는지 살펴보자


/* ScriptEngine.swift */
func wait(duration: Double) {
    NSThread.sleepForTimeInterval(duration)
}

이 wait함수는 engineQueue에 의해 관리되는 분리된 스레드에서 호출될것이 보장된다. 그래서 sleepForTimeInterval은 메인스레드가 아니라 이 스레드에서 동작할것이다. 자바스크립트는 실행이 잠시 중단될것이다.


다음은 좀 복잡한 대기로직들이다.


/* ScriptEngine.swift */
let tapCondition = NSCondition()
let levelEndCondition = NSCondition()
var seenTap = false
var seenLevelEnd = false

func waitForCondition(condition: NSCondition, 
        inout predicate: Bool) {
    condition.lock()
    while (!predicate) { // loop to protect against spurious wakes
        condition.wait()
    }
    predicate = false
    condition.unlock()
}

func notifyCondition(condition: NSCondition) {
    condition.lock()
    condition.signal()
    condition.unlock()
}

func waitForTap() {
    waitForCondition(tapCondition, &seenTap)
}

func notifyTap() {
    notifyCondition(tapCondition)
}

func waitForLevelEnd() {
    waitForCondition(levelEndCondition, &seenLevelEnd)
}

func notifyLevelEnd() {
    notifyCondition(levelEndCondition)
}

waitForTap()과 waitForLevelEnd()는 자바스크립에서 대응되는 함수가 호출되었을때 인스턴스 메서드로 노출된다. waitForCondition(condition:predicate:)는 해당 컨디션이 시그널될때까지 스레드를 블럭한다. notifyTap(), notifyLevelEnd()는 해당 컨티션발생시 시그널되고 engineQueue스레드를 깨워서 자바스크립트가 다시 살아나게 한다. 이 메서드들은 게임엔진에서 해당 이벤트가 발생하였을때 호출해야 한다.


이제 남은 노출된 함수는 구현하기에 따라 다르며 여기서는 다음과 같이 구현하였다.


/* ScriptEngine.swift */
func postNotification(type: GameNotification, 
        var userInfo: [NSObject : AnyObject]?) {
    
    if userInfo == nil {
        userInfo = [NSObject : AnyObject]()
    }
    
    userInfo![kGameNotificationUserInfoCurrentScriptName] 
        =  self.currentScriptName
    
    dispatch_async(dispatch_get_main_queue(), {
        NSNotificationCenter.defaultCenter()
            .postNotificationName(
                type.name(), object: self, userInfo: userInfo)
    })
}

func loadLevel(levelName: String) {
    postNotification(GameNotification.LoadLevel, 
        userInfo: [kGameNotificationUserInfoLevelName: levelName])
}

func showText(text: String, _ x: Double, _ y: Double) {
    postNotification(GameNotification.ShowText, 
        userInfo: [kGameNotificationUserInfoText: text, 
                   kGameNotificationUserInfoTextX: x, 
                   kGameNotificationUserInfoTextY: y])
}

func hideText() {
    postNotification(GameNotification.HideText, nil)
}


Posted by 삼스