Skip to content

Commit 8c529e8

Browse files
committed
feat: add contentpass context to sdk
1 parent a085722 commit 8c529e8

File tree

9 files changed

+314
-280
lines changed

9 files changed

+314
-280
lines changed

sharedExample/src/App.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { StyleSheet, View } from 'react-native';
2-
import { ContentpassSdkProvider } from './ContentpassContext';
32
import ContentpassUsage from './ContentpassUsage';
3+
import { ContentpassSdkProvider } from 'react-native-contentpass';
4+
import { contentpassConfig } from './contentpassConfig';
45

56
const styles = StyleSheet.create({
67
container: {
@@ -17,7 +18,7 @@ const styles = StyleSheet.create({
1718

1819
export default function App() {
1920
return (
20-
<ContentpassSdkProvider>
21+
<ContentpassSdkProvider contentpassConfig={contentpassConfig}>
2122
<View style={styles.container}>
2223
<ContentpassUsage />
2324
</View>

sharedExample/src/ContentpassContext.tsx

Lines changed: 0 additions & 43 deletions
This file was deleted.

sharedExample/src/ContentpassUsage.tsx

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import { useContentpassSdk } from './ContentpassContext';
21
import { Button, ScrollView, StyleSheet, Text, View } from 'react-native';
32
import { useEffect, useRef, useState } from 'react';
4-
import type { ContentpassState } from 'react-native-contentpass';
3+
import {
4+
type ContentpassState,
5+
useContentpassSdk,
6+
} from 'react-native-contentpass';
57
import {
68
SPConsentManager,
79
type SPUserData,
810
} from '@sourcepoint/react-native-cmp';
11+
import setupSourcepoint from './setupSourcepoint';
912

1013
const styles = StyleSheet.create({
1114
sourcepointDataContainer: {
@@ -23,27 +26,6 @@ const styles = StyleSheet.create({
2326
},
2427
});
2528

26-
const sourcePointConfig = {
27-
accountId: 375,
28-
propertyId: 37858,
29-
propertyName: 'mobile.cmpsourcepoint.demo',
30-
};
31-
32-
const setupSourcepoint = (hasValidSubscription: boolean) => {
33-
const { accountId, propertyName, propertyId } = sourcePointConfig;
34-
const spConsentManager = new SPConsentManager();
35-
36-
spConsentManager.build(accountId, propertyId, propertyName, {
37-
gdpr: {
38-
targetingParams: {
39-
acps: hasValidSubscription ? 'true' : 'false',
40-
},
41-
},
42-
});
43-
44-
return spConsentManager;
45-
};
46-
4729
export default function ContentpassUsage() {
4830
const [authResult, setAuthResult] = useState<ContentpassState | undefined>();
4931
const contentpassSdk = useContentpassSdk();
@@ -78,6 +60,7 @@ export default function ContentpassUsage() {
7860
const onContentpassStateChange = (state: ContentpassState) => {
7961
setAuthResult(state);
8062
};
63+
8164
contentpassSdk.registerObserver(onContentpassStateChange);
8265

8366
return () => {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { SPConsentManager } from '@sourcepoint/react-native-cmp';
2+
3+
const sourcePointConfig = {
4+
accountId: 375,
5+
propertyId: 37858,
6+
propertyName: 'mobile.cmpsourcepoint.demo',
7+
};
8+
9+
const setupSourcepoint = (hasValidSubscription: boolean) => {
10+
const { accountId, propertyName, propertyId } = sourcePointConfig;
11+
const spConsentManager = new SPConsentManager();
12+
13+
spConsentManager.build(accountId, propertyId, propertyName, {
14+
gdpr: {
15+
targetingParams: {
16+
acps: hasValidSubscription ? 'true' : 'false',
17+
},
18+
},
19+
});
20+
21+
return spConsentManager;
22+
};
23+
24+
export default setupSourcepoint;

src/Contentpass.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import OidcAuthStateStorage, {
2+
type OidcAuthState,
3+
} from './OidcAuthStateStorage';
4+
import {
5+
type ContentpassState,
6+
ContentpassStateType,
7+
} from './types/ContentpassState';
8+
import {
9+
authorize,
10+
type AuthorizeResult,
11+
refresh,
12+
} from 'react-native-app-auth';
13+
import { REFRESH_TOKEN_RETRIES, SCOPES } from './consts/oidcConsts';
14+
import { RefreshTokenStrategy } from './types/RefreshTokenStrategy';
15+
import fetchContentpassToken from './utils/fetchContentpassToken';
16+
import validateSubscription from './utils/validateSubscription';
17+
import type { ContentpassConfig } from './types/ContentpassConfig';
18+
19+
export type ContentpassObserver = (state: ContentpassState) => void;
20+
21+
export default class Contentpass {
22+
private authStateStorage: OidcAuthStateStorage;
23+
private readonly config: ContentpassConfig;
24+
25+
private contentpassState: ContentpassState = {
26+
state: ContentpassStateType.INITIALISING,
27+
};
28+
private contentpassStateObservers: ContentpassObserver[] = [];
29+
private oidcAuthState: OidcAuthState | null = null;
30+
private refreshTimer: NodeJS.Timeout | null = null;
31+
32+
constructor(config: ContentpassConfig) {
33+
this.authStateStorage = new OidcAuthStateStorage(config.propertyId);
34+
this.config = config;
35+
this.initialiseAuthState();
36+
}
37+
38+
public authenticate = async (): Promise<void> => {
39+
let result: AuthorizeResult;
40+
41+
try {
42+
result = await authorize({
43+
clientId: this.config.propertyId,
44+
redirectUrl: this.config.redirectUrl,
45+
issuer: this.config.issuer,
46+
scopes: SCOPES,
47+
additionalParameters: {
48+
cp_route: 'login',
49+
prompt: 'consent',
50+
cp_property: this.config.propertyId,
51+
},
52+
});
53+
} catch (err: any) {
54+
// FIXME: logger for error
55+
56+
this.changeContentpassState({
57+
state: ContentpassStateType.ERROR,
58+
error: 'message' in err ? err.message : 'Unknown error',
59+
});
60+
return;
61+
}
62+
63+
await this.onNewAuthState(result);
64+
};
65+
66+
public registerObserver(observer: ContentpassObserver) {
67+
if (this.contentpassStateObservers.includes(observer)) {
68+
return;
69+
}
70+
71+
observer(this.contentpassState);
72+
this.contentpassStateObservers.push(observer);
73+
}
74+
75+
public unregisterObserver(observer: ContentpassObserver) {
76+
this.contentpassStateObservers = this.contentpassStateObservers.filter(
77+
(o) => o !== observer
78+
);
79+
}
80+
81+
public logout = async () => {
82+
await this.authStateStorage.clearOidcAuthState();
83+
this.changeContentpassState({
84+
state: ContentpassStateType.UNAUTHENTICATED,
85+
hasValidSubscription: false,
86+
});
87+
};
88+
89+
public recoverFromError = async () => {
90+
this.changeContentpassState({
91+
state: ContentpassStateType.INITIALISING,
92+
});
93+
94+
await this.initialiseAuthState();
95+
};
96+
97+
private initialiseAuthState = async () => {
98+
const authState = await this.authStateStorage.getOidcAuthState();
99+
if (authState) {
100+
await this.onNewAuthState(authState);
101+
return;
102+
}
103+
104+
this.changeContentpassState({
105+
state: ContentpassStateType.UNAUTHENTICATED,
106+
hasValidSubscription: false,
107+
});
108+
};
109+
110+
private onNewAuthState = async (authState: OidcAuthState) => {
111+
this.oidcAuthState = authState;
112+
await this.authStateStorage.storeOidcAuthState(authState);
113+
114+
const strategy = this.setupRefreshTimer();
115+
if (strategy !== RefreshTokenStrategy.TIMER_SET) {
116+
return;
117+
}
118+
119+
try {
120+
const contentpassToken = await fetchContentpassToken({
121+
issuer: this.config.issuer,
122+
propertyId: this.config.propertyId,
123+
idToken: this.oidcAuthState.idToken,
124+
});
125+
const hasValidSubscription = validateSubscription(contentpassToken);
126+
this.changeContentpassState({
127+
state: ContentpassStateType.AUTHENTICATED,
128+
hasValidSubscription,
129+
});
130+
} catch (err: any) {
131+
this.changeContentpassState({
132+
state: ContentpassStateType.ERROR,
133+
error: err.message || 'Unknown error',
134+
});
135+
}
136+
};
137+
138+
private setupRefreshTimer = (): RefreshTokenStrategy => {
139+
const accessTokenExpirationDate =
140+
this.oidcAuthState?.accessTokenExpirationDate;
141+
142+
if (!accessTokenExpirationDate) {
143+
return RefreshTokenStrategy.NO_REFRESH;
144+
}
145+
146+
const now = new Date();
147+
const expirationDate = new Date(accessTokenExpirationDate);
148+
const timeDiff = expirationDate.getTime() - now.getTime();
149+
if (timeDiff <= 0) {
150+
this.refreshToken(0);
151+
return RefreshTokenStrategy.INSTANTLY;
152+
}
153+
154+
if (this.refreshTimer) {
155+
clearTimeout(this.refreshTimer);
156+
}
157+
158+
this.refreshTimer = setTimeout(async () => {
159+
await this.refreshToken(0);
160+
}, timeDiff);
161+
162+
return RefreshTokenStrategy.TIMER_SET;
163+
};
164+
165+
private refreshToken = async (counter: number) => {
166+
if (!this.oidcAuthState?.refreshToken) {
167+
return;
168+
}
169+
170+
try {
171+
const refreshResult = await refresh(
172+
{
173+
clientId: this.config.propertyId,
174+
redirectUrl: this.config.redirectUrl,
175+
issuer: this.config.issuer,
176+
scopes: SCOPES,
177+
},
178+
{
179+
refreshToken: this.oidcAuthState.refreshToken,
180+
}
181+
);
182+
await this.onNewAuthState(refreshResult);
183+
} catch (err) {
184+
await this.onRefreshTokenError(counter, err);
185+
}
186+
};
187+
188+
// @ts-expect-error remove when err starts being used
189+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
190+
private onRefreshTokenError = async (counter: number, err: unknown) => {
191+
// FIXME: logger for error
192+
// FIXME: add handling for specific error to not retry in every case
193+
if (counter <= REFRESH_TOKEN_RETRIES) {
194+
const delay = counter * 1000 * 10;
195+
await new Promise((resolve) => setTimeout(resolve, delay));
196+
await this.refreshToken(counter + 1);
197+
return;
198+
}
199+
200+
this.changeContentpassState({
201+
state: ContentpassStateType.UNAUTHENTICATED,
202+
hasValidSubscription: false,
203+
});
204+
await this.authStateStorage.clearOidcAuthState();
205+
};
206+
207+
private changeContentpassState = (state: ContentpassState) => {
208+
this.contentpassState = state;
209+
this.contentpassStateObservers.forEach((observer) => observer(state));
210+
211+
return this.contentpassState;
212+
};
213+
}

0 commit comments

Comments
 (0)