Slide 1

Slide 1 text

App Store Connect API iOSDC Reject Conference Day 1 @giginet

Slide 2

Slide 2 text

• ৄղFastfile • iOSDC 2018 2೔໨ϨΪϡϥʔτʔΫ • https://speakerdeck.com/giginet/xiang-jie-fastfile • • iOSDC 2018 3೔໨LT • https://speakerdeck.com/giginet/-15 • ୀ۶ͳ͜ͱ͸AppStore Connect APIʹ΍ΒͤΑ͏ • iOSDC Reject Conference 2018

Slide 3

Slide 3 text

• What's New in App Store Connect • https://developer.apple.com/videos/play/ wwdc2018/301/ • Automating App Store Connect • https://developer.apple.com/videos/play/ wwdc2018/303/

Slide 4

Slide 4 text

New Feature Summary App Store Connect API Unified user management App delivery on Linux TestFlight public links Sales and Trends overview App Store Connect for iOS Free trials for paid apps NEW

Slide 5

Slide 5 text

API͕ग़ͨʂʂʂ

Slide 6

Slide 6 text

Lifecycle Manage Users Design and Develop Deliver Provision Analyze Prepare App Store Metadata Distribute App Store Connect API Beta Test

Slide 7

Slide 7 text

apps betaLicenseAgreements builds buildBetaDetails betaAppReviewSubmissions betaAppLocalizations betaBuildLocalizations betaAppReviewDetails appEncryptionDeclarations userInvitations bundleIds devices certificates salesReports financeReports betaTesterInvitations buildDeliveries betaGroups users profiles betaTesters

Slide 8

Slide 8 text

> api.appstoreconnect.apple.com/v1/users/17cbd794-94a3-c7b0-1051 > GET HTTP/1.1 200 OK { "data": { "type": "users", "id": "17cbd794-94a3-c7b0-1051", "attributes": { "firstName": "Kate", "lastName": "Bell", "email": "[email protected]", … }, "relationships": {…}, "links": { "self": "…/v1/users/17cbd794-94a3-c7b0-1051" } } ]

Slide 9

Slide 9 text

{ "data": { "type": "userInvitations", "attributes": { "firstName": "John", "lastName": "Appleseed", "email": "[email protected]", "roles": ["DEVELOPER"], "allAppsVisible": true } } } > > POST /v1/userInvitations

Slide 10

Slide 10 text

{ "data": { "type": "users", "id": "24e811a2-2ad0-46e4-b632", "attributes": { "roles": ["DEVELOPER", "MARKETING"] } } } > > PATCH /v1/users/24e811a2-2ad0-46e4-b632

Slide 11

Slide 11 text

> > DELETE /v1/users/24e811a2-2ad0-46e4-b632 HTTP/1.1 204 NO CONTENT

Slide 12

Slide 12 text

> > DELETE /v1/users/24e811a2-2ad0-46e4-b632 HTTP/1.1 204 NO CONTENT

Slide 13

Slide 13 text

88%$ऴྃ௚ޙ

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

·ͩެ։͞Εͯͳ͍ 2018/9/18ݱࡏ

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

fastlane/spaceship • fastlaneͷҰ෦ • Ruby • Developer Center/iTunes Connectͷඇެ։APIΛୟ͍ͯΔ

Slide 20

Slide 20 text

giginet/Wormhole https://github.com/giginet/Wormhole

Slide 21

Slide 21 text

giginet/Wormhole ✅ 1VSF4XJGUZ ✅ 'BTUFSUIBO4QBDFTIJQ ✅ 5ZQF4BGFE ✅ 4VQQPSU4XJGU1BDLBHF.BOBHFS -JOVY4VQQPSU

Slide 22

Slide 22 text

giginet/Wormhole ✅ 1VSF4XJGUZ ✅ 'BTUFSUIBO4QBDFTIJQ ✅ 5ZQF4BGFE ✅ 4VQQPSU4XJGU1BDLBHF.BOBHFS -JOVY4VQQPSU ☠ "WBJMBCMFUIJT4VNNFS

Slide 23

Slide 23 text

࢖͍ํ 1.SwiftPMͰCommand Line ToolΛ࡞੒ 2.WormholeΛґଘʹ૊ΈࠐΉ 3.εΫϦϓτΛSwiftͰॻ͘

Slide 24

Slide 24 text

ύοέʔδੜ੒

Slide 25

Slide 25 text

// swift-tools-version:4.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "MyExecutable", dependencies: [ .package(url: "https://github.com/giginet/ Wormhole.git", .from("0.1.0")), ], targets: [ .target( name: "MyExecutable", dependencies: ["Wormhole"]), ] ) EFQFOEFODZ௥Ճ

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

import Foundation import Wormhole import Result let client = try! Client( p8Path: URL(fileURLWithPath: "key.p8"), issuerID: UUID(uuidString: "b91d85c7- b7db-4451-8f3f-9a3c8af9a392")!, keyID: “100000" ) *TTVFS*% ,FZ*% 1SJWBUF,FZ͕ඞཁ

Slide 28

Slide 28 text

HTTP/1.1 200 OK { "data": [{ "type": "users", "id": "17cbd794-94a3-c7b0-1051", "attributes": { "firstName": "Kate", "lastName": "Bell", "email": "[email protected]", … }, "relationships": {…}, "links": { "self": "https://api.appstoreconnect.apple.com/v1/users/17cbd794-94a3-c7b0-1 } }, { api.appstoreconnect.apple.com/v1/users/17cbd794-94a3-c7b0-1 > GET api.appstoreconnect.apple.com/v1/users

Slide 29

Slide 29 text

enum Role: String, Codable { case developer = "DEVELOPER" case marketing = "MARKETING" } struct User: AttributeType { let firstName: String let lastName: String let email: String let roles: [Role] }

Slide 30

Slide 30 text

HTTP/1.1 200 OK { "data": [{ "type": "users", "id": "17cbd794-94a3-c7b0-1051", "attributes": { "firstName": "Kate", "lastName": "Bell", "email": "[email protected]", … }, "relationships": {…}, "links": { "self": "https://api.appstoreconnect.apple.com/v1/users/17cbd794-94a3-c7b0-1 } }, { api.appstoreconnect.apple.com/v1/users/17cbd794-94a3-c7b0-1 > GET api.appstoreconnect.apple.com/v1/users

Slide 31

Slide 31 text

struct GetUsersRequest: RequestType { typealias Response = CollectionContainer let method: HTTPMethod = .get let path = "/users" let payload: RequestPayload = .void } ෳ਺ͷ݁ՌΛड͚औΔܕ 3FRVFTUCPEZ͸ۭ

Slide 32

Slide 32 text

let request = GetUsersRequest() client.send(request) { (result: Result, ClientError>) in switch result { case .success(let container): let firstUser: User = container.data.first! print("Name: \(firstUser.firstName) \ (firstUser.lastName)") print("Email: \(firstUser.email)") print("Roles: \(firstUser.roles)") case .failure(let error): print("Something went wrong") print(String(describing: error)) } } ϢʔβʔͷҰཡ͕औΕΔ

Slide 33

Slide 33 text

{ "data": { "type": "users", "id": "24e811a2-2ad0-46e4-b632", "attributes": { "roles": ["DEVELOPER", "MARKETING"] } } } > > PATCH /v1/users/24e811a2-2ad0-46e4-b632

Slide 34

Slide 34 text

struct RoleModificationRequest: RequestType { typealias Response = SingleContainer let method: HTTPMethod = .patch var path: String { return "/users/\(id.uuidString.lowercased())" } let id: UUID let roles: [Role] var payload: RequestPayload { return .init(id: id, type: "users", attributes: roles) } } ͭͷ݁ՌΛड͚औΔܕ 3FRVFTUCPEZΛఆٛ

Slide 35

Slide 35 text

{ "data": { "type": "users", "id": "24e811a2-2ad0-46e4-b632", "attributes": { "roles": ["DEVELOPER", "MARKETING"] } } } > > PATCH /v1/users/24e811a2-2ad0-46e4-b632

Slide 36

Slide 36 text

let uuid = UUID(uuidString: “588ec36e- ba74-11e8-8879-93c782f9ccb3")! let request = RoleModificationRequest( id: uuid, roles: [.developer, .marketing] ) client.send(request) { result in switch result { case .success(let container): let modifiedUser: User = container.data print("Name: \(modifiedUser.firstName) \(modifiedUser.lastName)") print("Email: \(modifiedUser.email)") print("Roles: \(modifiedUser.roles)") case .failure(let error): print("Something went wrong") print(String(describing: error)) } }

Slide 37

Slide 37 text

ೝূ

Slide 38

Slide 38 text

JSON Web Token • JSON Web Token • https://jwt.io/ • JSONΛbase64Τϯίʔυͯ͠ɺൿີ伴Λ࢖ͬͯ҉߸Խ • ެ։伴Λ࢖ͬͯ෮߸

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

JWT on Swift • Swift੡ͷ·ͱ΋ͳϥΠϒϥϦ͕ͳ͍ • ͋ͬͯ΋ػೳ͕଍Γͳ͍ • ECDSA(ES256)ʹඇରԠ • ೚ҙͷϔομʔΛ࣋ͨͤΒΕͳ͍ • OpenSSLͰࣗ෼Ͱ࣮૷͢Δͱେม

Slide 41

Slide 41 text

JWT on Swift • libjwtͱ͍͏CͷϥΠϒϥϦΛSwiftPMܦ༝ͰೖΕͯ࢖ͬͯ ͍Δ • libjwtࣗମͷ࠷৽൛͸HomebrewͰग़ͯͳ͍ͷͰࣗ෼Ͱ Formulaॻ͍ͨ • SwiftPM 4.2ͷsystemLibraryͱ͍͏࢓૊ΈΛ࢖͍ͬͯΔ • See Wormhole/JWTEncoder.swift

Slide 42

Slide 42 text

import JWT struct JWTEncoder { func encode(issuerID: UUID, keyID: String) throws -> String { let object = UnsafeMutablePointer.allocate(capacity: MemoryLayout.size) jwt_new(object) defer { jwt_free(object.pointee) } let keyPointer = convertToCString(privateKey) defer { keyPointer.deallocate() } jwt_set_alg(object.pointee, JWT_ALG_ES256, keyPointer, Int32(privateKey.utf16.count + 1)) // https://github.com/benmcollins/libjwt/pull/71 jwt_add_header(object.pointee, "kid", keyID) jwt_add_grant(object.pointee, "iss", issuerID.uuidString.lowercased()) let expirationDate = Date().addingTimeInterval(expirationInterval) jwt_add_grant_int(object.pointee, "exp", Int(expirationDate.timeIntervalSince1970)) jwt_add_grant(object.pointee, "aud", "appstoreconnect-v1") guard let encodedCString = jwt_encode_str(object.pointee) else { throw Error.decodeError } return String(cString: encodedCString) } } 4ZTUFN-JCSBSZͱͯ͠ެ։ $ͷؔ਺͕4XJGU͔Β࢖͑Δ

Slide 43

Slide 43 text

• Θ͍Θ͍Swift Package Manager • https://speakerdeck.com/giginet/waiwaiswift-package- manager • Θ͍Θ͍swiftc #5

Slide 44

Slide 44 text

·ͱΊ

Slide 45

Slide 45 text

࢖͍ಓʢΞΠσΟΞʣ • App Store Connectͷݖݶ؅ཧࣗಈԽʁ • TestFlight΁ͷট଴΍഑৴ͷࣗಈԽʁ • Wormhole + VaporͰొ࿥ϖʔδͱ͔࡞Εͦ͏ • ূ໌ॻ΍Provisioning Profileͷੜ੒ࣗಈԽʁ

Slide 46

Slide 46 text

໾ʹཱͭʁ • υΩϡϝϯτ͕·ͩग़ͯͳͯ͘શ๴͕Θ͔Βͳ͍ • ·ͩՆͰ͸ͳ͍ • ݱঢ়Ͱ΋શͯfastlane(spaceship)ͰͰ͖ͦ͏ɻ͜Ε͔Β

Slide 47

Slide 47 text

·ͱΊ • App Store Connect API͕·΋ͳ͘དྷͦ͏ • APIΫϥΠΞϯτ࡞ͬͨ • giginet/Wormhole • ࢖͑ΔΑ͏ʹͳͬͨΒ΍͍ͬͯ͘

Slide 48

Slide 48 text

2018/9/17ݱࡏ

Slide 49

Slide 49 text

͝ਗ਼ௌ ͋Γ͕ͱ͏͍͟͝·ͨ͠