1- import { authorize , type AuthorizeResult } from 'react-native-app-auth' ;
2- import { SCOPES } from './oidcConsts' ;
3- import OidcAuthStateStorage from './OidcAuthStateStorage' ;
4- import parseContentpassToken from './utils/parseContentpassToken' ;
1+ import {
2+ authorize ,
3+ type AuthorizeResult ,
4+ refresh ,
5+ } from 'react-native-app-auth' ;
6+ import { REFRESH_TOKEN_RETRIES , SCOPES } from './consts/oidcConsts' ;
7+ import OidcAuthStateStorage , {
8+ type OidcAuthState ,
9+ } from './OidcAuthStateStorage' ;
510import fetchContentpassToken from './utils/fetchContentpassToken' ;
11+ import {
12+ type ContentpassState ,
13+ ContentpassStateType ,
14+ } from './types/ContentpassState' ;
15+ import type { ContentpassConfig } from './types/ContentpassConfig' ;
16+ import validateSubscription from './utils/validateSubscription' ;
17+ import { RefreshTokenStrategy } from './types/RefreshTokenStrategy' ;
618
7- export type Config = {
8- propertyId : string ;
9- redirectUrl : string ;
10- issuer : string ;
11- } ;
12-
13- export enum State {
14- INITIALISING = 'INITIALISING' ,
15- UNAUTHENTICATED = 'UNAUTHENTICATED' ,
16- AUTHENTICATED = 'AUTHENTICATED' ,
17- ERROR = 'ERROR' ,
18- }
19-
20- type ErrorAuthenticateResult = {
21- state : State . ERROR ;
22- hasValidSubscription : false ;
23- error : Error ;
24- } ;
19+ export type {
20+ ContentpassState ,
21+ ErrorState ,
22+ AuthenticatedState ,
23+ InitialisingState ,
24+ UnauthenticatedState ,
25+ } from './types/ContentpassState' ;
2526
26- type StandardAuthenticateResult = {
27- state : State ;
28- hasValidSubscription : boolean ;
29- error ?: never ;
30- } ;
27+ export type { ContentpassConfig } from './types/ContentpassConfig' ;
3128
32- export type AuthenticateResult =
33- | StandardAuthenticateResult
34- | ErrorAuthenticateResult ;
29+ export type ContentpassObserver = ( state : ContentpassState ) => void ;
3530
3631export class Contentpass {
3732 private authStateStorage : OidcAuthStateStorage ;
38- private readonly config : Config ;
39- private state : State = State . INITIALISING ;
33+ private readonly config : ContentpassConfig ;
34+
35+ private contentpassState : ContentpassState = {
36+ state : ContentpassStateType . INITIALISING ,
37+ } ;
38+ private contentpassStateObservers : ContentpassObserver [ ] = [ ] ;
39+ private oidcAuthState : OidcAuthState | null = null ;
40+ private refreshTimer : NodeJS . Timeout | null = null ;
4041
41- constructor ( config : Config ) {
42+ constructor ( config : ContentpassConfig ) {
4243 this . authStateStorage = new OidcAuthStateStorage ( config . propertyId ) ;
4344 this . config = config ;
45+ this . initialiseAuthState ( ) ;
4446 }
4547
46- public async authenticate ( ) : Promise < AuthenticateResult > {
48+ public authenticate = async ( ) : Promise < void > = > {
4749 let result : AuthorizeResult ;
4850
4951 try {
@@ -61,40 +63,160 @@ export class Contentpass {
6163 } catch ( err : any ) {
6264 // FIXME: logger for error
6365
64- return {
65- state : State . ERROR ,
66- hasValidSubscription : false ,
66+ this . changeContentpassState ( {
67+ state : ContentpassStateType . ERROR ,
6768 error : 'message' in err ? err . message : 'Unknown error' ,
68- } ;
69+ } ) ;
70+ return ;
71+ }
72+
73+ await this . onNewAuthState ( result ) ;
74+ } ;
75+
76+ public registerObserver ( observer : ContentpassObserver ) {
77+ if ( this . contentpassStateObservers . includes ( observer ) ) {
78+ return ;
79+ }
80+
81+ this . contentpassStateObservers . push ( observer ) ;
82+ }
83+
84+ public unregisterObserver ( observer : ContentpassObserver ) {
85+ this . contentpassStateObservers = this . contentpassStateObservers . filter (
86+ ( o ) => o !== observer
87+ ) ;
88+ }
89+
90+ public logout = async ( ) => {
91+ await this . authStateStorage . clearOidcAuthState ( ) ;
92+ this . changeContentpassState ( {
93+ state : ContentpassStateType . UNAUTHENTICATED ,
94+ hasValidSubscription : false ,
95+ } ) ;
96+ } ;
97+
98+ public recoverFromError = async ( ) => {
99+ this . changeContentpassState ( {
100+ state : ContentpassStateType . INITIALISING ,
101+ } ) ;
102+
103+ await this . initialiseAuthState ( ) ;
104+ } ;
105+
106+ private initialiseAuthState = async ( ) => {
107+ const authState = await this . authStateStorage . getOidcAuthState ( ) ;
108+ if ( authState ) {
109+ await this . onNewAuthState ( authState ) ;
110+ return ;
69111 }
70112
71- this . state = State . AUTHENTICATED ;
72- await this . authStateStorage . storeOidcAuthState ( result ) ;
113+ this . changeContentpassState ( {
114+ state : ContentpassStateType . UNAUTHENTICATED ,
115+ hasValidSubscription : false ,
116+ } ) ;
117+ } ;
118+
119+ private onNewAuthState = async ( authState : OidcAuthState ) => {
120+ this . oidcAuthState = authState ;
121+ await this . authStateStorage . storeOidcAuthState ( authState ) ;
122+
123+ const strategy = this . setupRefreshTimer ( ) ;
124+ if ( strategy !== RefreshTokenStrategy . TIMER_SET ) {
125+ return ;
126+ }
73127
74128 try {
75129 const contentpassToken = await fetchContentpassToken ( {
76130 issuer : this . config . issuer ,
77131 propertyId : this . config . propertyId ,
78- idToken : result . idToken ,
132+ idToken : this . oidcAuthState . idToken ,
79133 } ) ;
80- const hasValidSubscription = this . validateSubscription ( contentpassToken ) ;
81-
82- return {
83- state : this . state ,
134+ const hasValidSubscription = validateSubscription ( contentpassToken ) ;
135+ this . changeContentpassState ( {
136+ state : ContentpassStateType . AUTHENTICATED ,
84137 hasValidSubscription,
85- } ;
138+ } ) ;
139+ } catch ( err : any ) {
140+ this . changeContentpassState ( {
141+ state : ContentpassStateType . ERROR ,
142+ error : err . message || 'Unknown error' ,
143+ } ) ;
144+ }
145+ } ;
146+
147+ private setupRefreshTimer = ( ) : RefreshTokenStrategy => {
148+ const accessTokenExpirationDate =
149+ this . oidcAuthState ?. accessTokenExpirationDate ;
150+
151+ if ( ! accessTokenExpirationDate ) {
152+ return RefreshTokenStrategy . NO_REFRESH ;
153+ }
154+
155+ const now = new Date ( ) ;
156+ const expirationDate = new Date ( accessTokenExpirationDate ) ;
157+ const timeDiff = expirationDate . getTime ( ) - now . getTime ( ) ;
158+ if ( timeDiff <= 0 ) {
159+ this . refreshToken ( 0 ) ;
160+ return RefreshTokenStrategy . INSTANTLY ;
161+ }
162+
163+ if ( this . refreshTimer ) {
164+ clearTimeout ( this . refreshTimer ) ;
165+ }
166+
167+ this . refreshTimer = setTimeout ( async ( ) => {
168+ await this . refreshToken ( 0 ) ;
169+ } , timeDiff ) ;
170+
171+ return RefreshTokenStrategy . TIMER_SET ;
172+ } ;
173+
174+ private refreshToken = async ( counter : number ) => {
175+ if ( ! this . oidcAuthState ?. refreshToken ) {
176+ return ;
177+ }
178+
179+ try {
180+ const refreshResult = await refresh (
181+ {
182+ clientId : this . config . propertyId ,
183+ redirectUrl : this . config . redirectUrl ,
184+ issuer : this . config . issuer ,
185+ scopes : SCOPES ,
186+ } ,
187+ {
188+ refreshToken : this . oidcAuthState . refreshToken ,
189+ }
190+ ) ;
191+ await this . onNewAuthState ( refreshResult ) ;
86192 } catch ( err ) {
87- // FIXME: logger for error
88- return {
89- state : this . state ,
90- hasValidSubscription : false ,
91- } ;
193+ await this . onRefreshTokenError ( counter , err ) ;
92194 }
93- }
195+ } ;
94196
95- private validateSubscription ( contentpassToken : string ) {
96- const { body } = parseContentpassToken ( contentpassToken ) ;
197+ // @ts -expect-error remove when err starts being used
198+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
199+ private onRefreshTokenError = async ( counter : number , err : unknown ) => {
200+ // FIXME: logger for error
201+ // FIXME: add handling for specific error to not retry in every case
202+ if ( counter <= REFRESH_TOKEN_RETRIES ) {
203+ const delay = counter * 1000 * 10 ;
204+ await new Promise ( ( resolve ) => setTimeout ( resolve , delay ) ) ;
205+ await this . refreshToken ( counter + 1 ) ;
206+ return ;
207+ }
97208
98- return ! ! body . auth && ! ! body . plans . length ;
99- }
209+ this . changeContentpassState ( {
210+ state : ContentpassStateType . UNAUTHENTICATED ,
211+ hasValidSubscription : false ,
212+ } ) ;
213+ await this . authStateStorage . clearOidcAuthState ( ) ;
214+ } ;
215+
216+ private changeContentpassState = ( state : ContentpassState ) => {
217+ this . contentpassState = state ;
218+ this . contentpassStateObservers . forEach ( ( observer ) => observer ( state ) ) ;
219+
220+ return this . contentpassState ;
221+ } ;
100222}
0 commit comments