import { SignInResult } from "./signin-result" ; import { SignInProvider } from "./signin-provider" ; export class AppleProvider implements SignInProvider { static readonly scriptUrl = "https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js" ; static readonly nameLocalStorageKeyPrefix = "pwa-auth-apple-email" ; constructor ( private clientId , private redirectUri ? ) { } signIn ( ) { return this . loadDependencies ( ) . then ( ( ) => this . signInWithApple ( ) ) ; } loadDependencies ( ) { return this . appendAppleScript ( ) . then ( ( ) => this . initAuth ( ) ) ; } private appendAppleScript ( ) { const apple = this . getAppleJS ( ) ; if ( ! apple ) { return new Promise < void > ( ( resolve , reject ) => { const scriptEl = window . document . createElement ( "script" ) ; scriptEl . async = true ; scriptEl . src = AppleProvider . scriptUrl ; scriptEl . onload = ( ) => resolve ( ) ; scriptEl . onerror = ( error ) => reject ( { message : "Error loading Apple JS" , error : error } ) ; window . document . head . appendChild ( scriptEl ) ; } ) ; } return Promise . resolve ( ) ; } private initAuth ( ) { const apple = this . getAppleJS ( ) ; if ( ! apple ) { return Promise . reject ( "AppleJS not loaded" ) ; } apple . auth . init ( { clientId : this . clientId , scope : "name email" , redirectURI : this . redirectUri || this . trimSlash ( location . href ) , state : "" , usePopup : true } ) ; return Promise . resolve ( ) ; } private signInWithApple ( ) { const apple = this . getAppleJS ( ) ; if ( ! apple ) { return Promise . reject ( "AppleJS not loaded" ) ; } return apple . auth . signIn ( ) . then ( result => this . getSignInResult ( result ) ) ; } private getAppleJS ( ) { return window [ "AppleID" ] as AppleSignInAPI . AppleID | undefined ; } private getSignInResult ( rawResult ) { if ( this . isErrorResult ( rawResult ) ) { return { error : new Error ( rawResult . error ) , provider : "Apple" } ; } const userDetails = this . decodeUserDetails ( rawResult ) ; return { email : userDetails . email , name : userDetails . name , accessToken : rawResult ?. authorization ?. code , accessTokenExpiration : userDetails . appleToken ? new Date ( userDetails . appleToken . exp * 1000 ) : null , imageUrl : null , providerData : rawResult , provider : "Apple" , error : null } ; } private isErrorResult ( result ) { return ! ! ( result as AppleSignInAPI . SignInErrorI ) . error ; } private decodeUserDetails ( result ) { const webToken = this . decodeJwt ( result . authorization . id_token ) as AppleJwtToken ; const email = webToken ?. email ; if ( ! email ) { throw new Error ( "Unable to decode user's email from JWT token: " + result . authorization . id_token ) ; } let name = null ; if ( result . user && result . user . name ) { name = [ result . user . name . firstName , result . user . name . lastName ] . filter ( n => ! ! n ) . join ( " " ) ; this . tryStoreNameWithEmail ( name , email ) ; } else { name = this . tryGetStoredNameFromEmail ( email ) ; } if ( ! name ) { name = "" ; } return { name , email , appleToken : webToken } ; } private decodeJwt ( token ) { const base64Url = token . split ( '.' ) [ 1 ] ; const base64 = base64Url . replace ( /-/g , '+' ) . replace ( /_/g , '/' ) ; const jsonPayload = decodeURIComponent ( atob ( base64 ) . split ( '' ) . map ( function ( c ) { return '%' + ( '00' + c . charCodeAt ( 0 ) . toString ( 16 ) ) . slice ( - 2 ) ; } ) . join ( '' ) ) ; return JSON . parse ( jsonPayload ) ; } private tryGetStoredNameFromEmail ( email ) { const key = this . getUserNameLocalStorageKey ( email ) ; try { return localStorage . getItem ( key ) ; } catch ( error ) { console . warn ( "Error fetching user from local storage." , key , error ) ; return null ; } } private tryStoreNameWithEmail ( name , email ) { const key = this . getUserNameLocalStorageKey ( email ) ; try { localStorage . setItem ( key , name ) ; } catch ( error ) { console . warn ( "Error storing user name in local storage. Subsequent sign-ins may not have a user name." , key , name , error ) ; } } private getUserNameLocalStorageKey ( email ) { return `${ AppleProvider . nameLocalStorageKeyPrefix }-${ email }` ; } private trimSlash ( input ) { let length = 0 ; for ( let i = input . length - 1 ; i >= 0 ; i -- ) { if ( input [ i ] !== '/' ) { length = i + 1 ; break ; } } return input . substr ( 0 , length ) ; } } type AppleJwtToken = { iss : string ; aud : string ; exp : number ; iat : number ; sub : string ; c_hash : string ; email : string ; email_verified : boolean ; auth_time : number ; nonce_supported : boolean ; } ;