Skip to content

Commit

Permalink
Receipt Migration URL scheme action pushed to legacy Blink
Browse files Browse the repository at this point in the history
  • Loading branch information
Carlos Cabanero committed Dec 17, 2021
1 parent 23a5fc0 commit 2060997
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 23 deletions.
1 change: 1 addition & 0 deletions Blink/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
<array>
<string>ssh</string>
<string>blinkshell</string>
<string>blinkv14</string>
</array>
</dict>
</array>
Expand Down
114 changes: 98 additions & 16 deletions Blink/Receipt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,91 @@ import Combine
import CryptoKit
import Foundation
import StoreKit
import SwiftUI


fileprivate let endpointURL = URL(string: "https://us-central1-gold-stone-332203.cloudfunctions.net/receiptEntitlement")!


struct ReceiptMigrationView: View {
var process: ReceiptMigrationProgress

var body: some View {
VStack {
Text("Receipt migration in progress.")
}
.onAppear(perform: process.load)
}
}

class ReceiptMigrationProgress: ObservableObject {
var receiptOperation: AnyCancellable? = nil
let originalUserId: String
@Published var state = State.working

enum State {
case working
case done
case requestFailure
case receiptFetchFailure
case migrationFailure
}

init(originalUserId: String) {
self.originalUserId = originalUserId
}

func load() {
receiptOperation = SKStore()
.fetchReceiptURLPublisher()
.tryMap { receiptURL -> String in
let d = try Data(contentsOf: receiptURL, options: .alwaysMapped)
let receipt = d.base64EncodedString(options: [])
return receipt
}
//Just("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")
.flatMap { MigrationToken.requestTokenForMigration(receipt: $0, attachedTo: self.originalUserId) }
.sink(
receiveCompletion: { completion in
// If successful, dismiss yourself
// Show errors and let the user dismiss
print(completion)
switch completion {
case .finished:
self.state = .done
case .failure(let error):
// SKStoreError
// ReceiptMigrationError
// SKStoreError.fetchError
// ReceiptMigrationError.requestError
print("Error performing request token migration - \(error)")
switch error {
case ReceiptMigrationError.requestError,
SKStoreError.fetchError:
self.state = .requestFailure
case is ReceiptMigrationError:
self.state = .migrationFailure
case is SKStoreError:
self.state = .receiptFetchFailure
default:
self.state = .requestFailure
}
}
},
receiveValue: { migrationToken in
// Open blinkv15 with received value
print(migrationToken)
}
)
}
}

struct MigrationToken: Codable {
let token: String
let data: String

public static func requestTokenForMigration(receiptData: String, attachedTo originalUserId: String) -> AnyPublisher<Data, Error> {
Just(["receiptData": receiptData,
public static func requestTokenForMigration(receipt: String, attachedTo originalUserId: String) -> AnyPublisher<Data, Error> {
Just(["receiptData": receipt,
"originalUserId": originalUserId])
// NOTE Leaving this for reference. This is now responsibility of other layers.
// SKStore()
Expand All @@ -67,23 +141,31 @@ struct MigrationToken: Codable {
URLSession.shared.dataTaskPublisher(for: $0)
.tryMap { element -> Data in
guard let httpResponse = element.response as? HTTPURLResponse else {
throw ReceiptMigrationError.RequestError
throw URLError(.badServerResponse)
}
let statusCode = httpResponse.statusCode
guard statusCode == 200 else {
let errorMessage = try? JSONDecoder().decode(ErrorMessage.self, from: element.data)
switch statusCode {
case 409:
throw ReceiptMigrationError.ReceiptExists
throw ReceiptMigrationError.receiptExists(errorMessage)
case 400:
throw ReceiptMigrationError.InvalidAppReceipt(error: errorMessage)
throw ReceiptMigrationError.invalidAppReceipt(errorMessage)
default:
throw ReceiptMigrationError.RequestError
throw ReceiptMigrationError.requestError(errorMessage)
}
}
return element.data
}
}.eraseToAnyPublisher()
}
// .mapError { error in
// if let error = error as? ReceiptMigrationError {
// return error
// } else {
// return ReceiptMigrationError.requestError(ErrorMessage(error: error.localizedDescription))
// }
// }
.eraseToAnyPublisher()
}

public func validateReceiptForMigration(attachedTo originalUserId: String) throws {
Expand All @@ -97,10 +179,10 @@ struct MigrationToken: Codable {
let receiptTimestamp = Int(dataComponents[2]),
// 60s margin for timestamp. It is rare that it takes more than 15 secs.
(currentTimestamp - receiptTimestamp) < 60 else {
throw ReceiptMigrationError.InvalidMigrationReceipt
throw ReceiptMigrationError.invalidMigrationReceipt
}
guard isSignatureVerified else {
throw ReceiptMigrationError.InvalidMigrationReceiptSignature
throw ReceiptMigrationError.invalidMigrationReceiptSignature
}
}

Expand Down Expand Up @@ -129,19 +211,19 @@ struct MigrationToken: Codable {
}
}

struct ErrorMessage: Codable, Equatable {
struct ErrorMessage: Codable {
let error: String
}

enum ReceiptMigrationError: Error, Equatable {
enum ReceiptMigrationError: Error {
// 409 - we may want to drop the ID in this scenario.
case ReceiptExists
case receiptExists(ErrorMessage?)
// 40X
case InvalidAppReceipt(error: ErrorMessage?)
case InvalidMigrationReceipt
case InvalidMigrationReceiptSignature
case invalidAppReceipt(ErrorMessage?)
case invalidMigrationReceipt
case invalidMigrationReceiptSignature
// Everything else
case RequestError
case requestError(ErrorMessage?)
}

enum SKStoreError: Error {
Expand Down
16 changes: 13 additions & 3 deletions Blink/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
Handles the `ssh://` URL schemes and x-callback-url for devices that are running iOS 13 or higher.
*/
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {

if let sshUrlScheme = URLContexts.first(where: { $0.url.scheme == "ssh" })?.url {
if let blinkUrlScheme = URLContexts.first(where: { $0.url.scheme == "blinkv14"})?.url {
_handleReceiptUrlScheme(with: blinkUrlScheme, calledReceivedBy: "test")
} else if let sshUrlScheme = URLContexts.first(where: { $0.url.scheme == "ssh" })?.url {
_handleSshUrlScheme(with: sshUrlScheme)
} else if let xCallbackUrl = URLContexts.first(where: { $0.url.scheme == "blinkshell" })?.url {
_handleXcallbackUrl(with: xCallbackUrl)
Expand Down Expand Up @@ -349,7 +350,16 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {

// MARK: Manage the `scene(_:openURLContexts:)` actions
extension SceneDelegate {

// blinkv14:validatereceipt?OriginalUserId
private func _handleReceiptUrlScheme(with blinkReceiptUrl: URL, calledReceivedBy: String) {
// TODO: Ignore it did not come from our Blink 15 AppID

// Start receipt exchange function.
let ctrl = UIHostingController(rootView: ReceiptMigrationView(process: ReceiptMigrationProgress(originalUserId: "test")))
ctrl.modalPresentationStyle = .formSheet
_spCtrl.present(ctrl, animated: false)
}

/**
Handles the `ssh://` URL schemes and x-callback-url for devices that are running iOS 13 or higher.
- Parameters:
Expand Down
11 changes: 7 additions & 4 deletions BlinkTests/ReceiptTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,18 @@ class ReceiptTests: XCTestCase {

XCTAssertThrowsError(try requestTokenForMigration(receiptData: validReceipt, attachedTo: "otherUser"),
"Deny user after one exists") { error in
XCTAssertEqual(error as? ReceiptMigrationError,
ReceiptMigrationError.ReceiptExists)
if case ReceiptMigrationError.receiptExists(let err) = error {
print(err)
} else {
XCTFail("Receipt already exists for user.")
}
}
}

func testInvalidReceiptMigration() throws {
XCTAssertThrowsError(try requestTokenForMigration(receiptData: invalidReceipt, attachedTo: validUser),
"Deny receipt that could not be validated by backend") { error in
if case ReceiptMigrationError.InvalidAppReceipt(let err) = error {
if case ReceiptMigrationError.invalidAppReceipt(let err) = error {
print(err)
} else {
XCTFail("Receipt should not validate from backend.")
Expand All @@ -64,7 +67,7 @@ class ReceiptTests: XCTestCase {
var migrationToken: MigrationToken!
var error: Error? = nil

var c = MigrationToken.requestTokenForMigration(receiptData: receiptData, attachedTo: user)
var c = MigrationToken.requestTokenForMigration(receipt: receiptData, attachedTo: user)
// In a regular application, the migration token is sent over a URL scheme,
// decoded and validated on that side.
.decode(type: MigrationToken.self, decoder: JSONDecoder())
Expand Down

0 comments on commit 2060997

Please sign in to comment.