@playbasis-ai/qwikcard-sdk 2.3.14 → 2.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playbasis-ai/qwikcard-sdk",
3
- "version": "2.3.14",
3
+ "version": "2.3.16",
4
4
  "description": "Playbasis SDK for QwikCard College Rewards - React Native gamification integration",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -27,6 +27,8 @@ interface PlaybasisProviderProps {
27
27
  tenantId?: string;
28
28
  playerId?: string;
29
29
  baseUrl?: string;
30
+ requestTimeoutMs?: number;
31
+ requestRetries?: number;
30
32
  }
31
33
 
32
34
  const QWIK_BASE_URLS = {
@@ -50,13 +52,22 @@ export function PlaybasisProvider({
50
52
  tenantId,
51
53
  playerId,
52
54
  baseUrl,
55
+ requestTimeoutMs,
56
+ requestRetries,
53
57
  }: PlaybasisProviderProps) {
54
58
  const resolvedTenantId =
55
59
  tenantId ?? (baseUrl && baseUrl.includes('production') ? 'qwik-prod' : 'qwikcard');
56
60
  const resolvedBaseUrl = resolveBaseUrl(resolvedTenantId, baseUrl);
57
61
  const client = useMemo(
58
- () => new PlaybasisClient({ apiKey, tenantId: resolvedTenantId, baseUrl: resolvedBaseUrl }),
59
- [apiKey, resolvedTenantId, resolvedBaseUrl],
62
+ () =>
63
+ new PlaybasisClient({
64
+ apiKey,
65
+ tenantId: resolvedTenantId,
66
+ baseUrl: resolvedBaseUrl,
67
+ timeoutMs: requestTimeoutMs,
68
+ retries: requestRetries,
69
+ }),
70
+ [apiKey, resolvedTenantId, resolvedBaseUrl, requestTimeoutMs, requestRetries],
60
71
  );
61
72
 
62
73
  const value = useMemo(
@@ -14,6 +14,8 @@ export interface QwikCardAppProps {
14
14
  playerId: string;
15
15
  baseUrl?: string;
16
16
  leaderboardId?: string;
17
+ requestTimeoutMs?: number;
18
+ requestRetries?: number;
17
19
  }
18
20
 
19
21
  function AppContent({ leaderboardId }: { leaderboardId?: string }) {
package/src/api/client.ts CHANGED
@@ -19,6 +19,8 @@ interface ClientConfig {
19
19
  tenantId: string;
20
20
  baseUrl?: string;
21
21
  fetchImpl?: typeof fetch;
22
+ timeoutMs?: number;
23
+ retries?: number;
22
24
  }
23
25
 
24
26
  const DEFAULT_BASE_URL = 'https://apim-pb-staging.azure-api.net/playbasis/v1';
@@ -32,12 +34,16 @@ export class PlaybasisClient {
32
34
  private readonly fetchImpl: typeof fetch;
33
35
  private readonly apiKey: string;
34
36
  private readonly tenantId: string;
37
+ private readonly timeoutMs: number;
38
+ private readonly retries: number;
35
39
 
36
40
  constructor(config: ClientConfig) {
37
41
  this.tenantId = config.tenantId;
38
42
  this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
39
43
  this.fetchImpl = config.fetchImpl ?? fetch;
40
44
  this.apiKey = config.apiKey;
45
+ this.timeoutMs = config.timeoutMs ?? 10000;
46
+ this.retries = config.retries ?? 2;
41
47
  }
42
48
 
43
49
  /**
@@ -59,54 +65,79 @@ export class PlaybasisClient {
59
65
  ): Promise<T> {
60
66
  const url = `${this.baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;
61
67
 
62
- // Build headers with Content-Type set before creating init
63
- const headers: Record<string, string> = {
64
- 'Ocp-Apim-Subscription-Key': this.apiKey,
65
- 'X-Tenant-ID': this.tenantId,
66
- ...options?.headers,
67
- };
68
-
69
- // Set Content-Type before assigning to init to avoid mutation after reference
70
- if (options?.body !== undefined) {
71
- headers['Content-Type'] = headers['Content-Type'] ?? 'application/json';
72
- }
68
+ const attemptRequest = async (): Promise<T> => {
69
+ const headers: Record<string, string> = {
70
+ 'Ocp-Apim-Subscription-Key': this.apiKey,
71
+ 'X-Tenant-ID': this.tenantId,
72
+ ...options?.headers,
73
+ };
73
74
 
74
- const init: RequestInit = {
75
- method,
76
- headers,
77
- body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
78
- };
75
+ if (options?.body !== undefined) {
76
+ headers['Content-Type'] = headers['Content-Type'] ?? 'application/json';
77
+ }
79
78
 
80
- const response = await this.fetchImpl(url, init);
81
- const text = await response.text();
79
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
80
+ const timeoutId = this.timeoutMs
81
+ ? setTimeout(() => controller?.abort(), this.timeoutMs)
82
+ : null;
82
83
 
83
- // Safely parse JSON - server may return non-JSON (e.g., HTML error page)
84
- let json: unknown;
85
- if (text) {
86
84
  try {
87
- json = JSON.parse(text);
88
- } catch {
89
- // Response is not valid JSON - leave as undefined
90
- json = undefined;
85
+ const init: RequestInit = {
86
+ method,
87
+ headers,
88
+ body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
89
+ signal: controller?.signal,
90
+ };
91
+
92
+ const response = await this.fetchImpl(url, init);
93
+ const text = await response.text();
94
+
95
+ let json: unknown;
96
+ if (text) {
97
+ try {
98
+ json = JSON.parse(text);
99
+ } catch {
100
+ json = undefined;
101
+ }
102
+ }
103
+
104
+ const getErrorMessage = (value: unknown): string | undefined => {
105
+ if (!value || typeof value !== 'object') return undefined;
106
+ if (!('message' in value)) return undefined;
107
+ const messageValue = (value as { message?: unknown }).message;
108
+ return typeof messageValue === 'string' ? messageValue : undefined;
109
+ };
110
+
111
+ if (!response.ok) {
112
+ const errorMessage =
113
+ getErrorMessage(json) ||
114
+ (text && !json ? text.slice(0, 200) : null) ||
115
+ `Request failed: ${response.status} ${response.statusText}`;
116
+ const error = new Error(String(errorMessage)) as Error & { status?: number };
117
+ error.status = response.status;
118
+ throw error;
119
+ }
120
+
121
+ return json as T;
122
+ } finally {
123
+ if (timeoutId) clearTimeout(timeoutId);
91
124
  }
92
- }
93
-
94
- const getErrorMessage = (value: unknown): string | undefined => {
95
- if (!value || typeof value !== 'object') return undefined;
96
- if (!('message' in value)) return undefined;
97
- const messageValue = (value as { message?: unknown }).message;
98
- return typeof messageValue === 'string' ? messageValue : undefined;
99
125
  };
100
126
 
101
- if (!response.ok) {
102
- const errorMessage =
103
- getErrorMessage(json) ||
104
- (text && !json ? text.slice(0, 200) : null) ||
105
- `Request failed: ${response.status} ${response.statusText}`;
106
- throw new Error(String(errorMessage));
127
+ for (let attempt = 0; attempt <= this.retries; attempt += 1) {
128
+ try {
129
+ return await attemptRequest();
130
+ } catch (error) {
131
+ const status = (error as { status?: number }).status;
132
+ const shouldRetry =
133
+ attempt < this.retries &&
134
+ (status === undefined || status === 429 || (status >= 500 && status <= 599));
135
+ if (!shouldRetry) throw error;
136
+ await new Promise((resolve) => setTimeout(resolve, 400 * (attempt + 1)));
137
+ }
107
138
  }
108
139
 
109
- return json as T;
140
+ throw new Error('Request failed after retries');
110
141
  }
111
142
 
112
143
  // ─────────────────────────────────────────────────────────────────────────