@playbasis-ai/qwikcard-sdk 2.3.14 → 2.3.15

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/CHANGELOG.md CHANGED
@@ -67,6 +67,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
67
67
 
68
68
  - Fallback badge image mapping for Qwik slugs when API omits image URLs.
69
69
 
70
+ ## [2.3.15] - 2026-01-30
71
+
72
+ ### Added
73
+
74
+ - Lightweight request retries/timeouts with opt-in configuration for smoother client UX.
75
+
70
76
  ## [2.3.11] - 2026-01-29
71
77
 
72
78
  ### Changed
@@ -12,8 +12,10 @@ interface PlaybasisProviderProps {
12
12
  tenantId?: string;
13
13
  playerId?: string;
14
14
  baseUrl?: string;
15
+ requestTimeoutMs?: number;
16
+ requestRetries?: number;
15
17
  }
16
- export declare function PlaybasisProvider({ children, apiKey, tenantId, playerId, baseUrl, }: PlaybasisProviderProps): import("react/jsx-runtime").JSX.Element;
18
+ export declare function PlaybasisProvider({ children, apiKey, tenantId, playerId, baseUrl, requestTimeoutMs, requestRetries, }: PlaybasisProviderProps): import("react/jsx-runtime").JSX.Element;
17
19
  export declare function usePlaybasis(): PlaybasisContextValue;
18
20
  export default PlaybasisProvider;
19
21
  //# sourceMappingURL=PlaybasisProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PlaybasisProvider.d.ts","sourceRoot":"","sources":["../src/PlaybasisProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAO/C,UAAU,qBAAqB;IAC7B,MAAM,EAAE,eAAe,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAQD,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,SAAS,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiBD,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,OAAO,GACR,EAAE,sBAAsB,2CAwBxB;AAMD,wBAAgB,YAAY,IAAI,qBAAqB,CAMpD;AAED,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"PlaybasisProvider.d.ts","sourceRoot":"","sources":["../src/PlaybasisProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAO/C,UAAU,qBAAqB;IAC7B,MAAM,EAAE,eAAe,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAQD,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,SAAS,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAiBD,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,gBAAgB,EAChB,cAAc,GACf,EAAE,sBAAsB,2CA+BxB;AAMD,wBAAgB,YAAY,IAAI,qBAAqB,CAMpD;AAED,eAAe,iBAAiB,CAAC"}
@@ -17,10 +17,16 @@ const resolveBaseUrl = (tenantId, baseUrl) => {
17
17
  // ─────────────────────────────────────────────────────────────────────────────
18
18
  // Provider Component
19
19
  // ─────────────────────────────────────────────────────────────────────────────
20
- export function PlaybasisProvider({ children, apiKey, tenantId, playerId, baseUrl, }) {
20
+ export function PlaybasisProvider({ children, apiKey, tenantId, playerId, baseUrl, requestTimeoutMs, requestRetries, }) {
21
21
  const resolvedTenantId = tenantId ?? (baseUrl && baseUrl.includes('production') ? 'qwik-prod' : 'qwikcard');
22
22
  const resolvedBaseUrl = resolveBaseUrl(resolvedTenantId, baseUrl);
23
- const client = useMemo(() => new PlaybasisClient({ apiKey, tenantId: resolvedTenantId, baseUrl: resolvedBaseUrl }), [apiKey, resolvedTenantId, resolvedBaseUrl]);
23
+ const client = useMemo(() => new PlaybasisClient({
24
+ apiKey,
25
+ tenantId: resolvedTenantId,
26
+ baseUrl: resolvedBaseUrl,
27
+ timeoutMs: requestTimeoutMs,
28
+ retries: requestRetries,
29
+ }), [apiKey, resolvedTenantId, resolvedBaseUrl, requestTimeoutMs, requestRetries]);
24
30
  const value = useMemo(() => ({
25
31
  client,
26
32
  tenantId: resolvedTenantId,
@@ -4,6 +4,8 @@ export interface QwikCardAppProps {
4
4
  playerId: string;
5
5
  baseUrl?: string;
6
6
  leaderboardId?: string;
7
+ requestTimeoutMs?: number;
8
+ requestRetries?: number;
7
9
  }
8
10
  export declare function QwikCardApp(props: QwikCardAppProps): import("react/jsx-runtime").JSX.Element;
9
11
  //# sourceMappingURL=QwikCardApp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"QwikCardApp.d.ts","sourceRoot":"","sources":["../src/QwikCardApp.tsx"],"names":[],"mappings":"AAUA,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAwRD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,2CAMlD"}
1
+ {"version":3,"file":"QwikCardApp.d.ts","sourceRoot":"","sources":["../src/QwikCardApp.tsx"],"names":[],"mappings":"AAUA,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAwRD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,2CAMlD"}
@@ -4,12 +4,16 @@ interface ClientConfig {
4
4
  tenantId: string;
5
5
  baseUrl?: string;
6
6
  fetchImpl?: typeof fetch;
7
+ timeoutMs?: number;
8
+ retries?: number;
7
9
  }
8
10
  export declare class PlaybasisClient {
9
11
  private readonly baseUrl;
10
12
  private readonly fetchImpl;
11
13
  private readonly apiKey;
12
14
  private readonly tenantId;
15
+ private readonly timeoutMs;
16
+ private readonly retries;
13
17
  constructor(config: ClientConfig);
14
18
  /**
15
19
  * Generate a unique ID for idempotency keys.
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,8 @@ export class PlaybasisClient {
8
8
  this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
9
9
  this.fetchImpl = config.fetchImpl ?? fetch;
10
10
  this.apiKey = config.apiKey;
11
+ this.timeoutMs = config.timeoutMs ?? 10000;
12
+ this.retries = config.retries ?? 2;
11
13
  }
12
14
  /**
13
15
  * Generate a unique ID for idempotency keys.
@@ -22,49 +24,74 @@ export class PlaybasisClient {
22
24
  }
23
25
  async request(method, path, options) {
24
26
  const url = `${this.baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;
25
- // Build headers with Content-Type set before creating init
26
- const headers = {
27
- 'Ocp-Apim-Subscription-Key': this.apiKey,
28
- 'X-Tenant-ID': this.tenantId,
29
- ...options?.headers,
30
- };
31
- // Set Content-Type before assigning to init to avoid mutation after reference
32
- if (options?.body !== undefined) {
33
- headers['Content-Type'] = headers['Content-Type'] ?? 'application/json';
34
- }
35
- const init = {
36
- method,
37
- headers,
38
- body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
39
- };
40
- const response = await this.fetchImpl(url, init);
41
- const text = await response.text();
42
- // Safely parse JSON - server may return non-JSON (e.g., HTML error page)
43
- let json;
44
- if (text) {
27
+ const attemptRequest = async () => {
28
+ const headers = {
29
+ 'Ocp-Apim-Subscription-Key': this.apiKey,
30
+ 'X-Tenant-ID': this.tenantId,
31
+ ...options?.headers,
32
+ };
33
+ if (options?.body !== undefined) {
34
+ headers['Content-Type'] = headers['Content-Type'] ?? 'application/json';
35
+ }
36
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
37
+ const timeoutId = this.timeoutMs
38
+ ? setTimeout(() => controller?.abort(), this.timeoutMs)
39
+ : null;
45
40
  try {
46
- json = JSON.parse(text);
41
+ const init = {
42
+ method,
43
+ headers,
44
+ body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
45
+ signal: controller?.signal,
46
+ };
47
+ const response = await this.fetchImpl(url, init);
48
+ const text = await response.text();
49
+ let json;
50
+ if (text) {
51
+ try {
52
+ json = JSON.parse(text);
53
+ }
54
+ catch {
55
+ json = undefined;
56
+ }
57
+ }
58
+ const getErrorMessage = (value) => {
59
+ if (!value || typeof value !== 'object')
60
+ return undefined;
61
+ if (!('message' in value))
62
+ return undefined;
63
+ const messageValue = value.message;
64
+ return typeof messageValue === 'string' ? messageValue : undefined;
65
+ };
66
+ if (!response.ok) {
67
+ const errorMessage = getErrorMessage(json) ||
68
+ (text && !json ? text.slice(0, 200) : null) ||
69
+ `Request failed: ${response.status} ${response.statusText}`;
70
+ const error = new Error(String(errorMessage));
71
+ error.status = response.status;
72
+ throw error;
73
+ }
74
+ return json;
47
75
  }
48
- catch {
49
- // Response is not valid JSON - leave as undefined
50
- json = undefined;
76
+ finally {
77
+ if (timeoutId)
78
+ clearTimeout(timeoutId);
51
79
  }
52
- }
53
- const getErrorMessage = (value) => {
54
- if (!value || typeof value !== 'object')
55
- return undefined;
56
- if (!('message' in value))
57
- return undefined;
58
- const messageValue = value.message;
59
- return typeof messageValue === 'string' ? messageValue : undefined;
60
80
  };
61
- if (!response.ok) {
62
- const errorMessage = getErrorMessage(json) ||
63
- (text && !json ? text.slice(0, 200) : null) ||
64
- `Request failed: ${response.status} ${response.statusText}`;
65
- throw new Error(String(errorMessage));
81
+ for (let attempt = 0; attempt <= this.retries; attempt += 1) {
82
+ try {
83
+ return await attemptRequest();
84
+ }
85
+ catch (error) {
86
+ const status = error.status;
87
+ const shouldRetry = attempt < this.retries &&
88
+ (status === undefined || status === 429 || (status >= 500 && status <= 599));
89
+ if (!shouldRetry)
90
+ throw error;
91
+ await new Promise((resolve) => setTimeout(resolve, 400 * (attempt + 1)));
92
+ }
66
93
  }
67
- return json;
94
+ throw new Error('Request failed after retries');
68
95
  }
69
96
  // ─────────────────────────────────────────────────────────────────────────
70
97
  // Players
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.15",
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
  // ─────────────────────────────────────────────────────────────────────────