@playbasis-ai/qwikcard-sdk 2.3.4

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.
Files changed (106) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/LICENSE +21 -0
  3. package/README.md +267 -0
  4. package/SDK_HANDOVER_GUIDE.md +129 -0
  5. package/dist/PlaybasisProvider.d.ts +19 -0
  6. package/dist/PlaybasisProvider.d.ts.map +1 -0
  7. package/dist/PlaybasisProvider.js +24 -0
  8. package/dist/QwikCardApp.d.ts +9 -0
  9. package/dist/QwikCardApp.d.ts.map +1 -0
  10. package/dist/QwikCardApp.js +210 -0
  11. package/dist/api/client.d.ts +66 -0
  12. package/dist/api/client.d.ts.map +1 -0
  13. package/dist/api/client.js +196 -0
  14. package/dist/components/Badge.d.ts +8 -0
  15. package/dist/components/Badge.d.ts.map +1 -0
  16. package/dist/components/Badge.js +34 -0
  17. package/dist/components/BadgeIcon.d.ts +10 -0
  18. package/dist/components/BadgeIcon.d.ts.map +1 -0
  19. package/dist/components/BadgeIcon.js +51 -0
  20. package/dist/components/Button.d.ts +10 -0
  21. package/dist/components/Button.d.ts.map +1 -0
  22. package/dist/components/Button.js +40 -0
  23. package/dist/components/GradientCard.d.ts +9 -0
  24. package/dist/components/GradientCard.d.ts.map +1 -0
  25. package/dist/components/GradientCard.js +28 -0
  26. package/dist/components/PointsBalance.d.ts +8 -0
  27. package/dist/components/PointsBalance.d.ts.map +1 -0
  28. package/dist/components/PointsBalance.js +85 -0
  29. package/dist/components/ProgressBar.d.ts +8 -0
  30. package/dist/components/ProgressBar.d.ts.map +1 -0
  31. package/dist/components/ProgressBar.js +41 -0
  32. package/dist/components/QuestProgress.d.ts +10 -0
  33. package/dist/components/QuestProgress.d.ts.map +1 -0
  34. package/dist/components/QuestProgress.js +94 -0
  35. package/dist/components/RadialGauge.d.ts +8 -0
  36. package/dist/components/RadialGauge.d.ts.map +1 -0
  37. package/dist/components/RadialGauge.js +53 -0
  38. package/dist/components/RewardCard.d.ts +10 -0
  39. package/dist/components/RewardCard.d.ts.map +1 -0
  40. package/dist/components/RewardCard.js +64 -0
  41. package/dist/components/RulesModal.d.ts +7 -0
  42. package/dist/components/RulesModal.d.ts.map +1 -0
  43. package/dist/components/RulesModal.js +106 -0
  44. package/dist/hooks/useBadges.d.ts +12 -0
  45. package/dist/hooks/useBadges.d.ts.map +1 -0
  46. package/dist/hooks/useBadges.js +42 -0
  47. package/dist/hooks/usePoints.d.ts +13 -0
  48. package/dist/hooks/usePoints.d.ts.map +1 -0
  49. package/dist/hooks/usePoints.js +70 -0
  50. package/dist/hooks/useQuests.d.ts +12 -0
  51. package/dist/hooks/useQuests.d.ts.map +1 -0
  52. package/dist/hooks/useQuests.js +41 -0
  53. package/dist/hooks/useQwikApp.d.ts +16 -0
  54. package/dist/hooks/useQwikApp.d.ts.map +1 -0
  55. package/dist/hooks/useQwikApp.js +42 -0
  56. package/dist/hooks/useRewards.d.ts +12 -0
  57. package/dist/hooks/useRewards.d.ts.map +1 -0
  58. package/dist/hooks/useRewards.js +56 -0
  59. package/dist/index.d.ts +27 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +29 -0
  62. package/dist/theme/context.d.ts +8 -0
  63. package/dist/theme/context.d.ts.map +1 -0
  64. package/dist/theme/context.js +8 -0
  65. package/dist/theme/tokens.d.ts +35 -0
  66. package/dist/theme/tokens.d.ts.map +1 -0
  67. package/dist/theme/tokens.js +33 -0
  68. package/dist/types/index.d.ts +119 -0
  69. package/dist/types/index.d.ts.map +1 -0
  70. package/dist/types/index.js +4 -0
  71. package/dist/web/widgetAssets.d.ts +4 -0
  72. package/dist/web/widgetAssets.d.ts.map +1 -0
  73. package/dist/web/widgetAssets.js +5 -0
  74. package/dist/web/widgetHtml.d.ts +2 -0
  75. package/dist/web/widgetHtml.d.ts.map +1 -0
  76. package/dist/web/widgetHtml.js +299 -0
  77. package/dist/web/widgetTypes.d.ts +128 -0
  78. package/dist/web/widgetTypes.d.ts.map +1 -0
  79. package/dist/web/widgetTypes.js +1 -0
  80. package/package.json +86 -0
  81. package/src/PlaybasisProvider.tsx +72 -0
  82. package/src/QwikCardApp.tsx +302 -0
  83. package/src/api/client.ts +307 -0
  84. package/src/components/Badge.tsx +51 -0
  85. package/src/components/BadgeIcon.tsx +97 -0
  86. package/src/components/Button.tsx +70 -0
  87. package/src/components/GradientCard.tsx +49 -0
  88. package/src/components/PointsBalance.tsx +122 -0
  89. package/src/components/ProgressBar.tsx +65 -0
  90. package/src/components/QuestProgress.tsx +153 -0
  91. package/src/components/RadialGauge.tsx +101 -0
  92. package/src/components/RewardCard.tsx +123 -0
  93. package/src/components/RulesModal.tsx +171 -0
  94. package/src/hooks/useBadges.ts +59 -0
  95. package/src/hooks/usePoints.ts +91 -0
  96. package/src/hooks/useQuests.ts +60 -0
  97. package/src/hooks/useQwikApp.ts +49 -0
  98. package/src/hooks/useRewards.ts +74 -0
  99. package/src/index.ts +34 -0
  100. package/src/theme/context.tsx +17 -0
  101. package/src/theme/tokens.ts +68 -0
  102. package/src/types/index.ts +176 -0
  103. package/src/web/widgetAssets.d.ts +3 -0
  104. package/src/web/widgetAssets.ts +6 -0
  105. package/src/web/widgetHtml.ts +302 -0
  106. package/src/web/widgetTypes.ts +146 -0
@@ -0,0 +1,210 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useMemo, useRef, useState } from 'react';
3
+ import { View, ActivityIndicator, Platform, Alert, TouchableOpacity, Text } from 'react-native';
4
+ import { WebView } from 'react-native-webview';
5
+ import { PlaybasisProvider, usePlaybasis } from './PlaybasisProvider';
6
+ import { useTheme } from './theme/context';
7
+ import { getWidgetHtml } from './web/widgetHtml';
8
+ function AppContent({ leaderboardId }) {
9
+ const theme = useTheme();
10
+ const { client, tenantId, playerId, baseUrl } = usePlaybasis();
11
+ const webViewRef = useRef(null);
12
+ const [activeGame, setActiveGame] = useState(null);
13
+ const isQwikDark = theme.colors.background.toLowerCase() === '#0f1419';
14
+ const gameHeaderStyle = {
15
+ flexDirection: 'row',
16
+ alignItems: 'center',
17
+ paddingHorizontal: 16,
18
+ paddingVertical: 12,
19
+ borderBottomWidth: 1,
20
+ borderBottomColor: isQwikDark ? '#e2e8f0' : theme.colors.surface,
21
+ backgroundColor: isQwikDark ? '#ffffff' : theme.colors.background,
22
+ };
23
+ const gameHeaderTextColor = isQwikDark ? '#0f172a' : theme.colors.text.primary;
24
+ const closeGame = () => {
25
+ setActiveGame(null);
26
+ webViewRef.current?.injectJavaScript("window.PlaybasisWidget?.useWidgetStore?.getState?.()?.setActiveTab?.('play'); true;");
27
+ };
28
+ const html = useMemo(() => getWidgetHtml(), []);
29
+ const buildBootstrapPayload = useCallback(async () => {
30
+ if (!playerId) {
31
+ throw new Error('Missing playerId for widget bootstrap');
32
+ }
33
+ const [player, balances, quests, allBadges, playerBadges, rewards] = await Promise.all([
34
+ client.getPlayer(playerId),
35
+ client.getBalances(playerId),
36
+ client.getPlayerQuests(playerId),
37
+ client.getBadges(),
38
+ client.getPlayerBadges(playerId),
39
+ client.getRewards().catch(() => []), // Gracefully handle if rewards endpoint doesn't exist
40
+ ]);
41
+ const xp = balances.find((b) => b.currency === 'xp')?.balance ?? 0;
42
+ const qwikCoins = balances.find((b) => b.currency === 'qwik-coins')?.balance ?? 0;
43
+ const leaderboardEntries = leaderboardId
44
+ ? await (async () => {
45
+ try {
46
+ const res = await client.getLeaderboard(leaderboardId, { limit: 50 });
47
+ return res.entries.map((e) => ({
48
+ userId: e.playerId,
49
+ rank: e.rank,
50
+ displayName: e.displayName,
51
+ score: e.score,
52
+ isCurrentUser: e.playerId === player.id,
53
+ }));
54
+ }
55
+ catch {
56
+ return [];
57
+ }
58
+ })()
59
+ : [];
60
+ return {
61
+ user: {
62
+ userId: player.id,
63
+ level: Math.max(1, Math.floor(xp / 1000) + 1),
64
+ exp: xp,
65
+ compositeScore: xp,
66
+ positiveChangePct: 0,
67
+ },
68
+ wallet: {
69
+ currency: 'XP',
70
+ balance: xp,
71
+ earnedDelta: 0,
72
+ qwikCoins,
73
+ },
74
+ goals: quests.map((q) => ({
75
+ goalId: q.id,
76
+ type: 'financial',
77
+ target: q.target,
78
+ current: q.progress,
79
+ progressPct: q.target > 0 ? Math.round((q.progress / q.target) * 100) : 0,
80
+ startDate: new Date().toISOString().slice(0, 10),
81
+ endDate: q.expiresAt ?? new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
82
+ status: q.status === 'completed' ? 'completed' : q.status === 'expired' ? 'at_risk' : 'on_track',
83
+ rewards: {
84
+ xp: q.rewards?.find((r) => r.type === 'points')?.amount || 100,
85
+ badges: (q.rewards
86
+ ?.filter((r) => r.type === 'badge')
87
+ .map((r) => r.badgeId)
88
+ .filter(Boolean) || []),
89
+ },
90
+ })),
91
+ badges: allBadges.map((b) => ({
92
+ id: b.id,
93
+ name: b.name,
94
+ rarity: 'common',
95
+ unlocked: playerBadges.some((pb) => pb.id === b.id && pb.isEarned),
96
+ imageUrl: b.imageUrl,
97
+ })),
98
+ rewards: rewards.map((r) => ({
99
+ id: r.id,
100
+ name: r.name,
101
+ description: r.description || '',
102
+ cost: r.cost || 0,
103
+ imageUrl: r.imageUrl,
104
+ })),
105
+ activities: [],
106
+ leaderboardEntries,
107
+ tenantId,
108
+ };
109
+ }, [client, leaderboardId, playerId, tenantId]);
110
+ const handleMessage = useCallback(async (event) => {
111
+ let message;
112
+ try {
113
+ message = JSON.parse(event.nativeEvent.data);
114
+ }
115
+ catch {
116
+ return;
117
+ }
118
+ if (message.type === 'ERROR') {
119
+ // Handle errors from WebView
120
+ console.error('[QwikCardApp] WebView Error:', message.error);
121
+ Alert.alert('Widget Error', message.error || 'An unknown error occurred in the widget.');
122
+ return;
123
+ }
124
+ if (message.type === 'OPEN_GAME') {
125
+ setActiveGame({ url: message.url, title: message.title });
126
+ return;
127
+ }
128
+ if (message.type !== 'REQUEST')
129
+ return;
130
+ const sendResponse = (payload) => {
131
+ const js = `window.__PB_HANDLE_NATIVE_MESSAGE__ && window.__PB_HANDLE_NATIVE_MESSAGE__(${JSON.stringify(payload)}); true;`;
132
+ webViewRef.current?.injectJavaScript(js);
133
+ };
134
+ const requestId = message.requestId;
135
+ if (!playerId) {
136
+ sendResponse({
137
+ type: 'RESPONSE',
138
+ requestId,
139
+ ok: false,
140
+ error: 'PlayerId is required for widget data requests',
141
+ });
142
+ return;
143
+ }
144
+ try {
145
+ switch (message.resource) {
146
+ case 'bootstrap': {
147
+ const data = await buildBootstrapPayload();
148
+ sendResponse({ type: 'RESPONSE', requestId, ok: true, data });
149
+ return;
150
+ }
151
+ default: {
152
+ sendResponse({
153
+ type: 'RESPONSE',
154
+ requestId,
155
+ ok: false,
156
+ error: `Unsupported resource: ${String(message.resource)}`,
157
+ });
158
+ return;
159
+ }
160
+ }
161
+ }
162
+ catch (error) {
163
+ sendResponse({
164
+ type: 'RESPONSE',
165
+ requestId,
166
+ ok: false,
167
+ error: error instanceof Error ? error.message : 'Failed to process widget request',
168
+ });
169
+ }
170
+ }, [buildBootstrapPayload, playerId]);
171
+ const injectedJavaScript = useMemo(() => {
172
+ // Seed initial config for the widget and establish a stable native->web message handler.
173
+ // The web layer will immediately request `bootstrap` from native.
174
+ const initPayload = {
175
+ type: 'INIT',
176
+ config: {
177
+ tenantId,
178
+ playerId: playerId ?? '',
179
+ baseUrl,
180
+ },
181
+ platform: Platform.OS,
182
+ };
183
+ return `window.__PB_INIT__ = ${JSON.stringify(initPayload)}; true;`;
184
+ }, [tenantId, playerId, baseUrl]);
185
+ if (!playerId) {
186
+ return (_jsx(View, { style: {
187
+ flex: 1,
188
+ alignItems: 'center',
189
+ justifyContent: 'center',
190
+ backgroundColor: theme.colors.background,
191
+ }, children: _jsx(ActivityIndicator, { size: "large", color: theme.colors.primary }) }));
192
+ }
193
+ return (_jsxs(View, { style: { flex: 1, backgroundColor: theme.colors.background }, children: [_jsx(View, { style: { flex: 1, display: activeGame ? 'none' : 'flex' }, children: _jsx(WebView, { ref: webViewRef, originWhitelist: ['*'], source: { html }, onMessage: handleMessage, injectedJavaScript: injectedJavaScript, javaScriptEnabled: true, domStorageEnabled: true, allowsInlineMediaPlayback: true, startInLoadingState: true, renderLoading: () => (_jsx(View, { style: {
194
+ flex: 1,
195
+ alignItems: 'center',
196
+ justifyContent: 'center',
197
+ backgroundColor: theme.colors.background,
198
+ }, children: _jsx(ActivityIndicator, { size: "large", color: theme.colors.primary }) })),
199
+ // Reduce common RN/WebView UX papercuts.
200
+ keyboardDisplayRequiresUserAction: false, setSupportMultipleWindows: false,
201
+ // Avoid white flashes on iOS.
202
+ style: { backgroundColor: theme.colors.background } }) }), activeGame && (_jsxs(View, { style: { flex: 1 }, children: [_jsxs(View, { style: gameHeaderStyle, children: [_jsx(TouchableOpacity, { onPress: closeGame, style: { paddingVertical: 4, paddingRight: 8 }, children: _jsx(Text, { style: { color: theme.colors.primary, fontWeight: '700' }, children: "\u2190 Arcade" }) }), _jsx(Text, { style: {
203
+ marginLeft: 4,
204
+ color: gameHeaderTextColor,
205
+ fontWeight: '700',
206
+ }, children: activeGame.title || 'Play' })] }), _jsx(WebView, { originWhitelist: ['*'], source: { uri: activeGame.url }, javaScriptEnabled: true, domStorageEnabled: true, allowsInlineMediaPlayback: true, startInLoadingState: true })] }))] }));
207
+ }
208
+ export function QwikCardApp(props) {
209
+ return (_jsx(PlaybasisProvider, { ...props, children: _jsx(AppContent, { leaderboardId: props.leaderboardId }) }));
210
+ }
@@ -0,0 +1,66 @@
1
+ import type { Badge, LeaderboardEntry, Player, PlayerRank, PointBalance, Quest, RedemptionResult, Reward, TrackEventInput } from '../types';
2
+ interface ClientConfig {
3
+ apiKey: string;
4
+ tenantId: string;
5
+ baseUrl?: string;
6
+ fetchImpl?: typeof fetch;
7
+ }
8
+ export declare class PlaybasisClient {
9
+ private readonly baseUrl;
10
+ private readonly fetchImpl;
11
+ private readonly apiKey;
12
+ private readonly tenantId;
13
+ constructor(config: ClientConfig);
14
+ /**
15
+ * Generate a unique ID for idempotency keys.
16
+ * Uses crypto.randomUUID when available, falls back to timestamp + random.
17
+ */
18
+ private generateId;
19
+ private request;
20
+ createPlayer(input: {
21
+ displayName: string;
22
+ email?: string;
23
+ metadata?: Record<string, unknown>;
24
+ }): Promise<Player>;
25
+ getPlayer(playerId: string): Promise<Player>;
26
+ updatePlayer(playerId: string, updates: Partial<{
27
+ displayName: string;
28
+ metadata: Record<string, unknown>;
29
+ }>): Promise<Player>;
30
+ getBalances(playerId: string): Promise<PointBalance[]>;
31
+ earnPoints(input: {
32
+ playerId: string;
33
+ currency: string;
34
+ amount: number;
35
+ reason?: string;
36
+ }): Promise<PointBalance>;
37
+ redeemPoints(input: {
38
+ playerId: string;
39
+ currency: string;
40
+ amount: number;
41
+ reason?: string;
42
+ }): Promise<PointBalance>;
43
+ trackEvent(input: TrackEventInput): Promise<{
44
+ eventId: string;
45
+ }>;
46
+ getQuests(): Promise<Quest[]>;
47
+ getPlayerQuests(playerId: string): Promise<Quest[]>;
48
+ getQuestProgress(playerId: string, questId: string): Promise<Quest>;
49
+ getBadges(): Promise<Badge[]>;
50
+ getPlayerBadges(playerId: string): Promise<Badge[]>;
51
+ getRewards(): Promise<Reward[]>;
52
+ redeemReward(playerId: string, rewardId: string): Promise<RedemptionResult>;
53
+ getLeaderboard(leaderboardId: string, options?: {
54
+ limit?: number;
55
+ cursor?: string;
56
+ }): Promise<{
57
+ entries: LeaderboardEntry[];
58
+ total: number;
59
+ }>;
60
+ getPlayerRank(leaderboardId: string, playerId: string): Promise<PlayerRank>;
61
+ health(): Promise<{
62
+ status: string;
63
+ }>;
64
+ }
65
+ export default PlaybasisClient;
66
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,196 @@
1
+ const DEFAULT_BASE_URL = 'https://apim-pb-staging.azure-api.net/playbasis/v1';
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // Client
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ export class PlaybasisClient {
6
+ constructor(config) {
7
+ this.tenantId = config.tenantId;
8
+ this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
9
+ this.fetchImpl = config.fetchImpl ?? fetch;
10
+ this.apiKey = config.apiKey;
11
+ }
12
+ /**
13
+ * Generate a unique ID for idempotency keys.
14
+ * Uses crypto.randomUUID when available, falls back to timestamp + random.
15
+ */
16
+ generateId() {
17
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
18
+ return crypto.randomUUID();
19
+ }
20
+ // Fallback for environments without crypto.randomUUID
21
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
22
+ }
23
+ async request(method, path, options) {
24
+ 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) {
45
+ try {
46
+ json = JSON.parse(text);
47
+ }
48
+ catch {
49
+ // Response is not valid JSON - leave as undefined
50
+ json = undefined;
51
+ }
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
+ };
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));
66
+ }
67
+ return json;
68
+ }
69
+ // ─────────────────────────────────────────────────────────────────────────
70
+ // Players
71
+ // ─────────────────────────────────────────────────────────────────────────
72
+ async createPlayer(input) {
73
+ const data = await this.request('POST', '/players', { body: input });
74
+ return data.player;
75
+ }
76
+ async getPlayer(playerId) {
77
+ const data = await this.request('GET', `/players/${encodeURIComponent(playerId)}`);
78
+ return data.player;
79
+ }
80
+ async updatePlayer(playerId, updates) {
81
+ const data = await this.request('PATCH', `/players/${encodeURIComponent(playerId)}`, {
82
+ body: updates,
83
+ });
84
+ return data.player;
85
+ }
86
+ // ─────────────────────────────────────────────────────────────────────────
87
+ // Points
88
+ // ─────────────────────────────────────────────────────────────────────────
89
+ async getBalances(playerId) {
90
+ const data = await this.request('GET', `/players/${encodeURIComponent(playerId)}/points`);
91
+ return data.balances;
92
+ }
93
+ async earnPoints(input) {
94
+ const idempotencyKey = `earn-${input.playerId}-${this.generateId()}`;
95
+ const data = await this.request('POST', '/points/earn', {
96
+ body: input,
97
+ headers: { 'Idempotency-Key': idempotencyKey },
98
+ });
99
+ return data.balance;
100
+ }
101
+ async redeemPoints(input) {
102
+ const idempotencyKey = `redeem-${input.playerId}-${this.generateId()}`;
103
+ const data = await this.request('POST', '/points/redeem', {
104
+ body: input,
105
+ headers: { 'Idempotency-Key': idempotencyKey },
106
+ });
107
+ return data.balance;
108
+ }
109
+ // ─────────────────────────────────────────────────────────────────────────
110
+ // Events
111
+ // ─────────────────────────────────────────────────────────────────────────
112
+ async trackEvent(input) {
113
+ const idempotencyKey = input.referenceId || `evt-${input.playerId}-${this.generateId()}`;
114
+ const event = {
115
+ id: idempotencyKey,
116
+ source: 'qwikcard-app',
117
+ type: input.type,
118
+ specversion: '1.0',
119
+ time: new Date().toISOString(),
120
+ subject: input.playerId,
121
+ data: input.data || {},
122
+ };
123
+ const data = await this.request('POST', '/events', {
124
+ body: event,
125
+ headers: { 'Idempotency-Key': idempotencyKey },
126
+ });
127
+ return { eventId: data.event?.id || idempotencyKey };
128
+ }
129
+ // ─────────────────────────────────────────────────────────────────────────
130
+ // Quests
131
+ // ─────────────────────────────────────────────────────────────────────────
132
+ async getQuests() {
133
+ const data = await this.request('GET', '/quests');
134
+ return data.quests || [];
135
+ }
136
+ async getPlayerQuests(playerId) {
137
+ const data = await this.request('GET', `/players/${encodeURIComponent(playerId)}/quests`);
138
+ return data.quests || [];
139
+ }
140
+ async getQuestProgress(playerId, questId) {
141
+ const data = await this.request('GET', `/players/${encodeURIComponent(playerId)}/quests/${encodeURIComponent(questId)}`);
142
+ return data.quest;
143
+ }
144
+ // ─────────────────────────────────────────────────────────────────────────
145
+ // Badges
146
+ // ─────────────────────────────────────────────────────────────────────────
147
+ async getBadges() {
148
+ const data = await this.request('GET', '/badges');
149
+ return data.badges || [];
150
+ }
151
+ async getPlayerBadges(playerId) {
152
+ const data = await this.request('GET', `/players/${encodeURIComponent(playerId)}/badges`);
153
+ return data.badges || [];
154
+ }
155
+ // ─────────────────────────────────────────────────────────────────────────
156
+ // Rewards
157
+ // ─────────────────────────────────────────────────────────────────────────
158
+ async getRewards() {
159
+ const data = await this.request('GET', '/rewards');
160
+ return data.rewards || [];
161
+ }
162
+ async redeemReward(playerId, rewardId) {
163
+ const idempotencyKey = `reward-${playerId}-${rewardId}-${this.generateId()}`;
164
+ const data = await this.request('POST', '/rewards/redeem', {
165
+ body: { playerId, rewardId },
166
+ headers: { 'Idempotency-Key': idempotencyKey },
167
+ });
168
+ return data;
169
+ }
170
+ // ─────────────────────────────────────────────────────────────────────────
171
+ // Leaderboards
172
+ // ─────────────────────────────────────────────────────────────────────────
173
+ async getLeaderboard(leaderboardId, options) {
174
+ const params = new URLSearchParams();
175
+ if (options?.limit)
176
+ params.set('limit', String(options.limit));
177
+ if (options?.cursor)
178
+ params.set('cursor', options.cursor);
179
+ const data = await this.request('GET', `/leaderboards/${encodeURIComponent(leaderboardId)}?${params}`);
180
+ return {
181
+ entries: data.leaderboard?.entries || [],
182
+ total: data.leaderboard?.total || 0,
183
+ };
184
+ }
185
+ async getPlayerRank(leaderboardId, playerId) {
186
+ const data = await this.request('GET', `/leaderboards/${encodeURIComponent(leaderboardId)}/players/${encodeURIComponent(playerId)}`);
187
+ return data.player;
188
+ }
189
+ // ─────────────────────────────────────────────────────────────────────────
190
+ // Health Check
191
+ // ─────────────────────────────────────────────────────────────────────────
192
+ async health() {
193
+ return this.request('GET', '/health');
194
+ }
195
+ }
196
+ export default PlaybasisClient;
@@ -0,0 +1,8 @@
1
+ interface BadgeProps {
2
+ label: string;
3
+ value?: string;
4
+ color?: string;
5
+ }
6
+ export declare function StatBadge({ label, value, color }: BadgeProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
8
+ //# sourceMappingURL=Badge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Badge.d.ts","sourceRoot":"","sources":["../../src/components/Badge.tsx"],"names":[],"mappings":"AAKA,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,UAAU,2CAmB5D"}
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { View, Text, StyleSheet } from 'react-native';
3
+ import { useTheme } from '../theme/context';
4
+ export function StatBadge({ label, value, color }) {
5
+ const theme = useTheme();
6
+ const accentColor = color || theme.colors.primary;
7
+ return (_jsxs(View, { style: [
8
+ styles.container,
9
+ {
10
+ backgroundColor: `${accentColor}1A`, // 10% opacity
11
+ borderColor: `${accentColor}33`, // 20% opacity
12
+ borderRadius: theme.borderRadius.m,
13
+ },
14
+ ], children: [_jsx(Text, { style: [styles.label, { color: theme.colors.text.secondary }], children: label }), value && _jsx(Text, { style: [styles.value, { color: accentColor }], children: value })] }));
15
+ }
16
+ const styles = StyleSheet.create({
17
+ container: {
18
+ paddingVertical: 6,
19
+ paddingHorizontal: 10,
20
+ borderWidth: 1,
21
+ alignItems: 'center',
22
+ justifyContent: 'center',
23
+ },
24
+ label: {
25
+ fontSize: 10,
26
+ textTransform: 'uppercase',
27
+ letterSpacing: 0.5,
28
+ marginBottom: 2,
29
+ },
30
+ value: {
31
+ fontSize: 14,
32
+ fontWeight: '700',
33
+ },
34
+ });
@@ -0,0 +1,10 @@
1
+ import type { Badge } from '../types';
2
+ interface BadgeIconProps {
3
+ badge: Badge;
4
+ size?: 'small' | 'medium' | 'large';
5
+ showName?: boolean;
6
+ style?: object;
7
+ }
8
+ export declare function BadgeIcon({ badge, size, showName, style }: BadgeIconProps): import("react/jsx-runtime").JSX.Element;
9
+ export default BadgeIcon;
10
+ //# sourceMappingURL=BadgeIcon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BadgeIcon.d.ts","sourceRoot":"","sources":["../../src/components/BadgeIcon.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAMtC,UAAU,cAAc;IACtB,KAAK,EAAE,KAAK,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,IAAe,EAAE,QAAe,EAAE,KAAK,EAAE,EAAE,cAAc,2CA6C3F;AA+BD,eAAe,SAAS,CAAC"}
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { View, Text, Image, StyleSheet } from 'react-native';
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // Component
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ export function BadgeIcon({ badge, size = 'medium', showName = true, style }) {
7
+ const dimensions = {
8
+ small: 40,
9
+ medium: 60,
10
+ large: 80,
11
+ };
12
+ const iconSize = dimensions[size];
13
+ const isEarned = badge.isEarned;
14
+ return (_jsxs(View, { style: [styles.container, style], children: [_jsx(View, { style: [
15
+ styles.iconContainer,
16
+ {
17
+ width: iconSize,
18
+ height: iconSize,
19
+ borderRadius: iconSize / 2,
20
+ opacity: isEarned ? 1 : 0.4,
21
+ },
22
+ ], children: badge.imageUrl ? (_jsx(Image, { source: { uri: badge.imageUrl }, style: { width: iconSize - 8, height: iconSize - 8, borderRadius: (iconSize - 8) / 2 }, resizeMode: "cover" })) : (_jsx(Text, { style: { fontSize: iconSize / 2 }, children: "\uD83C\uDFC5" })) }), showName && (_jsx(Text, { style: [styles.name, { color: isEarned ? '#1F2937' : '#9CA3AF' }], numberOfLines: 2, children: badge.name })), isEarned && badge.earnedAt && (_jsx(Text, { style: styles.earnedDate, children: new Date(badge.earnedAt).toLocaleDateString() }))] }));
23
+ }
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+ // Styles
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+ const styles = StyleSheet.create({
28
+ container: {
29
+ alignItems: 'center',
30
+ padding: 8,
31
+ width: 80,
32
+ },
33
+ iconContainer: {
34
+ backgroundColor: '#F3F4F6',
35
+ justifyContent: 'center',
36
+ alignItems: 'center',
37
+ marginBottom: 4,
38
+ },
39
+ name: {
40
+ fontSize: 11,
41
+ fontWeight: '500',
42
+ textAlign: 'center',
43
+ marginTop: 4,
44
+ },
45
+ earnedDate: {
46
+ fontSize: 9,
47
+ color: '#9CA3AF',
48
+ marginTop: 2,
49
+ },
50
+ });
51
+ export default BadgeIcon;
@@ -0,0 +1,10 @@
1
+ import { ViewStyle } from 'react-native';
2
+ interface ButtonProps {
3
+ title: string;
4
+ onPress: () => void;
5
+ variant?: 'primary' | 'secondary';
6
+ style?: ViewStyle;
7
+ }
8
+ export declare function Button({ title, onPress, variant, style }: ButtonProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=Button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../src/components/Button.tsx"],"names":[],"mappings":"AACA,OAAO,EAAsC,SAAS,EAAE,MAAM,cAAc,CAAC;AAK7E,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,SAAS,GAAG,WAAW,CAAC;IAClC,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAED,wBAAgB,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAmB,EAAE,KAAK,EAAE,EAAE,WAAW,2CAoCjF"}
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { TouchableOpacity, Text, StyleSheet } from 'react-native';
3
+ import LinearGradient from 'react-native-linear-gradient';
4
+ import { useTheme } from '../theme/context';
5
+ export function Button({ title, onPress, variant = 'primary', style }) {
6
+ const theme = useTheme();
7
+ if (variant === 'secondary') {
8
+ return (_jsx(TouchableOpacity, { onPress: onPress, style: [
9
+ styles.container,
10
+ {
11
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
12
+ borderColor: theme.colors.surface,
13
+ borderWidth: 1,
14
+ borderRadius: theme.borderRadius.m,
15
+ },
16
+ style,
17
+ ], children: _jsx(Text, { style: [styles.text, { color: theme.colors.text.primary }], children: title }) }));
18
+ }
19
+ // Primary variant with gradient
20
+ return (_jsx(TouchableOpacity, { onPress: onPress, style: [styles.touchable, style], children: _jsx(LinearGradient, { colors: theme.colors.gradients.button, start: { x: 0, y: 0 }, end: { x: 1, y: 0 }, style: [styles.container, { borderRadius: theme.borderRadius.m }], children: _jsx(Text, { style: [styles.text, { color: theme.colors.text.onPrimary }], children: title }) }) }));
21
+ }
22
+ const styles = StyleSheet.create({
23
+ touchable: {
24
+ shadowColor: '#FF4500',
25
+ shadowOffset: { width: 0, height: 4 },
26
+ shadowOpacity: 0.3,
27
+ shadowRadius: 8,
28
+ elevation: 4,
29
+ },
30
+ container: {
31
+ paddingVertical: 12,
32
+ paddingHorizontal: 20,
33
+ alignItems: 'center',
34
+ justifyContent: 'center',
35
+ },
36
+ text: {
37
+ fontSize: 14,
38
+ fontWeight: '700',
39
+ },
40
+ });
@@ -0,0 +1,9 @@
1
+ import { ReactNode } from 'react';
2
+ import { ViewStyle, StyleProp } from 'react-native';
3
+ interface GradientCardProps {
4
+ children: ReactNode;
5
+ style?: StyleProp<ViewStyle>;
6
+ }
7
+ export declare function GradientCard({ children, style }: GradientCardProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=GradientCard.d.ts.map