@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.
- package/CHANGELOG.md +142 -0
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/SDK_HANDOVER_GUIDE.md +129 -0
- package/dist/PlaybasisProvider.d.ts +19 -0
- package/dist/PlaybasisProvider.d.ts.map +1 -0
- package/dist/PlaybasisProvider.js +24 -0
- package/dist/QwikCardApp.d.ts +9 -0
- package/dist/QwikCardApp.d.ts.map +1 -0
- package/dist/QwikCardApp.js +210 -0
- package/dist/api/client.d.ts +66 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +196 -0
- package/dist/components/Badge.d.ts +8 -0
- package/dist/components/Badge.d.ts.map +1 -0
- package/dist/components/Badge.js +34 -0
- package/dist/components/BadgeIcon.d.ts +10 -0
- package/dist/components/BadgeIcon.d.ts.map +1 -0
- package/dist/components/BadgeIcon.js +51 -0
- package/dist/components/Button.d.ts +10 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Button.js +40 -0
- package/dist/components/GradientCard.d.ts +9 -0
- package/dist/components/GradientCard.d.ts.map +1 -0
- package/dist/components/GradientCard.js +28 -0
- package/dist/components/PointsBalance.d.ts +8 -0
- package/dist/components/PointsBalance.d.ts.map +1 -0
- package/dist/components/PointsBalance.js +85 -0
- package/dist/components/ProgressBar.d.ts +8 -0
- package/dist/components/ProgressBar.d.ts.map +1 -0
- package/dist/components/ProgressBar.js +41 -0
- package/dist/components/QuestProgress.d.ts +10 -0
- package/dist/components/QuestProgress.d.ts.map +1 -0
- package/dist/components/QuestProgress.js +94 -0
- package/dist/components/RadialGauge.d.ts +8 -0
- package/dist/components/RadialGauge.d.ts.map +1 -0
- package/dist/components/RadialGauge.js +53 -0
- package/dist/components/RewardCard.d.ts +10 -0
- package/dist/components/RewardCard.d.ts.map +1 -0
- package/dist/components/RewardCard.js +64 -0
- package/dist/components/RulesModal.d.ts +7 -0
- package/dist/components/RulesModal.d.ts.map +1 -0
- package/dist/components/RulesModal.js +106 -0
- package/dist/hooks/useBadges.d.ts +12 -0
- package/dist/hooks/useBadges.d.ts.map +1 -0
- package/dist/hooks/useBadges.js +42 -0
- package/dist/hooks/usePoints.d.ts +13 -0
- package/dist/hooks/usePoints.d.ts.map +1 -0
- package/dist/hooks/usePoints.js +70 -0
- package/dist/hooks/useQuests.d.ts +12 -0
- package/dist/hooks/useQuests.d.ts.map +1 -0
- package/dist/hooks/useQuests.js +41 -0
- package/dist/hooks/useQwikApp.d.ts +16 -0
- package/dist/hooks/useQwikApp.d.ts.map +1 -0
- package/dist/hooks/useQwikApp.js +42 -0
- package/dist/hooks/useRewards.d.ts +12 -0
- package/dist/hooks/useRewards.d.ts.map +1 -0
- package/dist/hooks/useRewards.js +56 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/theme/context.d.ts +8 -0
- package/dist/theme/context.d.ts.map +1 -0
- package/dist/theme/context.js +8 -0
- package/dist/theme/tokens.d.ts +35 -0
- package/dist/theme/tokens.d.ts.map +1 -0
- package/dist/theme/tokens.js +33 -0
- package/dist/types/index.d.ts +119 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/web/widgetAssets.d.ts +4 -0
- package/dist/web/widgetAssets.d.ts.map +1 -0
- package/dist/web/widgetAssets.js +5 -0
- package/dist/web/widgetHtml.d.ts +2 -0
- package/dist/web/widgetHtml.d.ts.map +1 -0
- package/dist/web/widgetHtml.js +299 -0
- package/dist/web/widgetTypes.d.ts +128 -0
- package/dist/web/widgetTypes.d.ts.map +1 -0
- package/dist/web/widgetTypes.js +1 -0
- package/package.json +86 -0
- package/src/PlaybasisProvider.tsx +72 -0
- package/src/QwikCardApp.tsx +302 -0
- package/src/api/client.ts +307 -0
- package/src/components/Badge.tsx +51 -0
- package/src/components/BadgeIcon.tsx +97 -0
- package/src/components/Button.tsx +70 -0
- package/src/components/GradientCard.tsx +49 -0
- package/src/components/PointsBalance.tsx +122 -0
- package/src/components/ProgressBar.tsx +65 -0
- package/src/components/QuestProgress.tsx +153 -0
- package/src/components/RadialGauge.tsx +101 -0
- package/src/components/RewardCard.tsx +123 -0
- package/src/components/RulesModal.tsx +171 -0
- package/src/hooks/useBadges.ts +59 -0
- package/src/hooks/usePoints.ts +91 -0
- package/src/hooks/useQuests.ts +60 -0
- package/src/hooks/useQwikApp.ts +49 -0
- package/src/hooks/useRewards.ts +74 -0
- package/src/index.ts +34 -0
- package/src/theme/context.tsx +17 -0
- package/src/theme/tokens.ts +68 -0
- package/src/types/index.ts +176 -0
- package/src/web/widgetAssets.d.ts +3 -0
- package/src/web/widgetAssets.ts +6 -0
- package/src/web/widgetHtml.ts +302 -0
- 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":"AAAA,OAAO,KAAK,EACV,KAAK,EACL,gBAAgB,EAChB,MAAM,EACN,UAAU,EACV,YAAY,EACZ,KAAK,EACL,gBAAgB,EAChB,MAAM,EACN,eAAe,EAChB,MAAM,UAAU,CAAC;AAMlB,UAAU,YAAY;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAQD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,MAAM,EAAE,YAAY;IAOhC;;;OAGG;IACH,OAAO,CAAC,UAAU;YAQJ,OAAO;IA6Df,YAAY,CAAC,KAAK,EAAE;QACxB,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpC,GAAG,OAAO,CAAC,MAAM,CAAC;IAKb,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQ5C,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,GAC3E,OAAO,CAAC,MAAM,CAAC;IAeZ,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAQtD,UAAU,CAAC,KAAK,EAAE;QACtB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,YAAY,CAAC;IASnB,YAAY,CAAC,KAAK,EAAE;QACxB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,YAAY,CAAC;IAanB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAsBhE,SAAS,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;IAK7B,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAQnD,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAYnE,SAAS,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;IAK7B,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAYnD,UAAU,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAK/B,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAa3E,cAAc,CAClB,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5C,OAAO,CAAC;QAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAcpD,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAY3E,MAAM,IAAI,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAG5C;AAED,eAAe,eAAe,CAAC"}
|
|
@@ -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 @@
|
|
|
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
|