@koehler8/cms-ext-crypto 1.0.0-beta.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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -0
  3. package/components/CommunityStrip.vue +655 -0
  4. package/components/CuriosityTeaser.vue +842 -0
  5. package/components/HeaderWalletAction.vue +398 -0
  6. package/components/HeroPresale.vue +778 -0
  7. package/components/PercentDoughnut.vue +249 -0
  8. package/components/Presale.vue +507 -0
  9. package/components/PresaleAdmin.vue +1259 -0
  10. package/components/PresaleFaq.vue +95 -0
  11. package/components/ProgressFomo.vue +766 -0
  12. package/components/SocialProofFeed.vue +585 -0
  13. package/components/Tokenomics.vue +617 -0
  14. package/components/Trust.vue +123 -0
  15. package/components/TrustBar.vue +619 -0
  16. package/components/presale/BonusIncentive.vue +476 -0
  17. package/components/presale/Buy.vue +5388 -0
  18. package/components/presale/CountdownTimer.vue +326 -0
  19. package/components/presale/FirstTimeOnboarding.vue +443 -0
  20. package/components/presale/FuseMeter.vue +276 -0
  21. package/components/presale/HoldersBenefits.vue +135 -0
  22. package/components/presale/MomentumCard.vue +163 -0
  23. package/components/presale/PresaleFaqContent.vue +393 -0
  24. package/components/presale/PriceTimeline.vue +143 -0
  25. package/components/presale/Stake.vue +1415 -0
  26. package/components/presale/Status.vue +1113 -0
  27. package/components/presale/StatusMetric.vue +336 -0
  28. package/components/presale/StatusProgressBar.vue +98 -0
  29. package/components/presale/TrustSignals.vue +595 -0
  30. package/components/presale/buyAnalytics.js +58 -0
  31. package/components/presale/buyAssets.js +75 -0
  32. package/components/presale/buyTextHelpers.js +582 -0
  33. package/components/presale/priceSnapshotCache.js +72 -0
  34. package/components/presale/useBuyContentConfig.js +643 -0
  35. package/components/presale/useBuyOnboarding.js +338 -0
  36. package/components/presale/useBuyTransaction.js +464 -0
  37. package/components/presale/useBuyWallet.js +509 -0
  38. package/components/presale/useFomoProgress.js +578 -0
  39. package/components/presale/walletBalanceHelper.js +47 -0
  40. package/composables/usePresaleContext.js +24 -0
  41. package/content.defaults.json +171 -0
  42. package/extension.config.json +116 -0
  43. package/index.js +8 -0
  44. package/package.json +47 -0
  45. package/plugins/appKit.js +261 -0
  46. package/setup.js +29 -0
  47. package/stores/presale.js +70 -0
  48. package/utils/presaleContracts.js +69 -0
  49. package/utils/scrollToPresale.js +21 -0
  50. package/utils/tokenFormat.js +21 -0
  51. package/utils/walletTracking.js +175 -0
@@ -0,0 +1,398 @@
1
+ <template>
2
+ <div class="header-wallet">
3
+ <button
4
+ type="button"
5
+ class="header-wallet__button primary-button"
6
+ :class="{
7
+ 'header-wallet__button--connected': isWalletConnected,
8
+ 'header-wallet__button--loading': isModalBusy
9
+ }"
10
+ :disabled="isModalBusy"
11
+ :aria-busy="isModalBusy ? 'true' : 'false'"
12
+ @click="handleWalletButtonClick"
13
+ >
14
+ <span class="header-wallet__icon" aria-hidden="true">
15
+ <svg
16
+ v-if="!isWalletConnected"
17
+ viewBox="0 0 18 18"
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ >
20
+ <rect
21
+ x="2.5"
22
+ y="5"
23
+ width="13"
24
+ height="8"
25
+ rx="2"
26
+ ry="2"
27
+ fill="none"
28
+ stroke="currentColor"
29
+ stroke-width="1.6"
30
+ stroke-linejoin="round"
31
+ />
32
+ <path
33
+ d="M15.5 7.5h-4a1.5 1.5 0 0 0 0 3h4"
34
+ fill="none"
35
+ stroke="currentColor"
36
+ stroke-width="1.6"
37
+ stroke-linecap="round"
38
+ stroke-linejoin="round"
39
+ />
40
+ <circle cx="12.5" cy="9" r="1" fill="currentColor" />
41
+ </svg>
42
+ <svg
43
+ v-else
44
+ viewBox="0 0 20 20"
45
+ xmlns="http://www.w3.org/2000/svg"
46
+ >
47
+ <circle
48
+ cx="10"
49
+ cy="10"
50
+ r="8"
51
+ fill="none"
52
+ stroke="currentColor"
53
+ stroke-width="1.6"
54
+ />
55
+ <path
56
+ d="m7.25 10.25 2.25 2.25 3.75-4.75"
57
+ fill="none"
58
+ stroke="currentColor"
59
+ stroke-width="1.6"
60
+ stroke-linecap="round"
61
+ stroke-linejoin="round"
62
+ />
63
+ </svg>
64
+ </span>
65
+ <span class="header-wallet__label">
66
+ <span v-if="isModalBusy" class="header-wallet__spinner" aria-hidden="true"></span>
67
+ {{ walletButtonLabel }}
68
+ </span>
69
+ </button>
70
+ </div>
71
+ </template>
72
+
73
+ <script setup>
74
+ import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from 'vue';
75
+ import { ChainController, CoreHelperUtil, ModalController } from '@reown/appkit-controllers';
76
+ import { useAppKitAccount } from '@reown/appkit/vue';
77
+ import { trackEvent, trackFunnelEvent } from '@koehler8/cms/utils/analytics';
78
+ import { markWalletDisconnected, peekPendingWalletConnectContext, setPendingWalletConnectContext, trackWalletConnectedOnce } from '../utils/walletTracking.js';
79
+ import { requestScrollToPresale } from '../utils/scrollToPresale.js';
80
+
81
+ const appKitAccount = useAppKitAccount();
82
+ const DEFAULT_CONNECT_LABEL = 'Connect Wallet';
83
+ const DEFAULT_LOADING_LABEL = 'Connecting\u2026';
84
+ const walletConnectLabel = ref(DEFAULT_CONNECT_LABEL);
85
+ const walletLoadingLabel = ref(DEFAULT_LOADING_LABEL);
86
+ const walletConnectedLabel = ref('');
87
+ const walletAddress = ref('');
88
+ const walletProfileName = ref('');
89
+ const isModalBusy = ref(false);
90
+ let caipAddressUnsubscribe;
91
+ let profileNameUnsubscribe;
92
+ let modalUnsubscribe;
93
+ let connectedWalletInfoUnsubscribe;
94
+ let hasTrackedAccountAddress = false;
95
+
96
+ const shortWalletAddress = computed(() => {
97
+ const value = walletAddress.value;
98
+ if (!value) return '';
99
+ if (value.length <= 10) return value;
100
+ return `${value.slice(0, 6)}\u2026${value.slice(-4)}`;
101
+ });
102
+
103
+ const isWalletConnected = computed(() => Boolean(walletAddress.value));
104
+
105
+ const walletButtonLabel = computed(() => {
106
+ if (isModalBusy.value) return walletLoadingLabel.value;
107
+ if (isWalletConnected.value) {
108
+ const baseName = walletProfileName.value || shortWalletAddress.value || walletAddress.value || '';
109
+ if (walletConnectedLabel.value) {
110
+ return walletConnectedLabel.value
111
+ .replace(/\{name\}/g, walletProfileName.value || baseName)
112
+ .replace(/\{address\}/g, shortWalletAddress.value || walletAddress.value || '');
113
+ }
114
+ return baseName || walletConnectLabel.value;
115
+ }
116
+ return walletConnectLabel.value;
117
+ });
118
+
119
+ const injectedSiteData = inject('siteData', ref({}));
120
+ const pageContent = inject('pageContent', ref({}));
121
+
122
+ function pickLabel(...candidates) {
123
+ for (const candidate of candidates) {
124
+ if (typeof candidate !== 'string') continue;
125
+ const trimmed = candidate.trim();
126
+ if (trimmed) return trimmed;
127
+ }
128
+ return '';
129
+ }
130
+
131
+ const normalizeWalletAddress = (value) => {
132
+ if (typeof value !== 'string') return '';
133
+ const trimmed = value.trim();
134
+ if (!trimmed) return '';
135
+ try {
136
+ if (CoreHelperUtil && typeof CoreHelperUtil.getPlainAddress === 'function') {
137
+ const plain = CoreHelperUtil.getPlainAddress(trimmed);
138
+ if (plain && typeof plain === 'string' && plain.trim()) return plain.trim();
139
+ }
140
+ } catch {}
141
+ return trimmed;
142
+ };
143
+
144
+ const normalizeChainId = (value) => {
145
+ if (typeof value === 'bigint') {
146
+ const numeric = Number(value);
147
+ return Number.isFinite(numeric) ? numeric : undefined;
148
+ }
149
+ if (typeof value === 'number') return Number.isFinite(value) ? value : undefined;
150
+ if (typeof value === 'string' && value.trim()) {
151
+ const numeric = Number(value);
152
+ return Number.isFinite(numeric) ? numeric : undefined;
153
+ }
154
+ return undefined;
155
+ };
156
+
157
+ const prettifyWalletProvider = (value) => {
158
+ if (typeof value !== 'string') return '';
159
+ const trimmed = value.trim();
160
+ if (!trimmed) return '';
161
+ const lower = trimmed.toLowerCase();
162
+ if (lower === 'metamask') return 'MetaMask';
163
+ if (lower === 'walletconnect') return 'WalletConnect';
164
+ if (lower === 'coinbase' || lower === 'coinbase wallet' || lower === 'coinbase_wallet') return 'Coinbase Wallet';
165
+ if (lower === 'trust wallet' || lower === 'trust_wallet') return 'Trust Wallet';
166
+ return trimmed;
167
+ };
168
+
169
+ const resolveWalletProviderName = () => {
170
+ let accountState;
171
+ if (typeof ChainController?.getAccountData === 'function') {
172
+ accountState = ChainController.getAccountData();
173
+ } else if (ChainController?.state?.activeChain && ChainController?.state?.chains?.get) {
174
+ accountState = ChainController.state.chains.get(ChainController.state.activeChain)?.accountState;
175
+ }
176
+ const info = accountState?.connectedWalletInfo;
177
+ const candidates = [info?.name, info?.label, info?.title, info?.id, info?.type];
178
+ for (const candidate of candidates) {
179
+ if (typeof candidate === 'string' && candidate.trim()) return prettifyWalletProvider(candidate);
180
+ }
181
+ return 'AppKit';
182
+ };
183
+
184
+ const dispatchScrollToPresale = (source = 'header') => {
185
+ requestScrollToPresale({ source, target: '#centerPresale', trigger: 'wallet_connected' });
186
+ };
187
+
188
+ const emitWalletConnectedAnalytics = () => {
189
+ if (!isWalletConnected.value) return;
190
+ const address = walletAddress.value;
191
+ if (!address) return;
192
+ const providerName = resolveWalletProviderName();
193
+ const activeChainId = normalizeChainId(ChainController?.state?.activeCaipNetwork?.id);
194
+ const pendingContext = peekPendingWalletConnectContext();
195
+ const resolvedSource = pendingContext?.source || 'header';
196
+ trackWalletConnectedOnce({
197
+ address,
198
+ providerName,
199
+ chainId: activeChainId,
200
+ source: resolvedSource,
201
+ onFirstConnect: dispatchScrollToPresale,
202
+ });
203
+ };
204
+
205
+ const setWalletAddress = (value, { skipAnalytics = false } = {}) => {
206
+ const normalized = normalizeWalletAddress(value);
207
+ if (!normalized) { resetWalletState(); return; }
208
+ walletAddress.value = normalized;
209
+ if (!skipAnalytics) emitWalletConnectedAnalytics();
210
+ };
211
+
212
+ const setWalletProfileName = (value, { skipAnalytics = false } = {}) => {
213
+ walletProfileName.value = typeof value === 'string' ? value.trim() : '';
214
+ if (!skipAnalytics) emitWalletConnectedAnalytics();
215
+ };
216
+
217
+ const resetWalletState = () => {
218
+ const previousAddress = walletAddress.value;
219
+ if (previousAddress) markWalletDisconnected(previousAddress);
220
+ walletAddress.value = '';
221
+ walletProfileName.value = '';
222
+ };
223
+
224
+ watch(() => appKitAccount.value.address, (nextAddress) => {
225
+ const hasAddress = typeof nextAddress === 'string' && nextAddress.trim();
226
+ if (hasAddress) {
227
+ const shouldSkipAnalytics = !hasTrackedAccountAddress;
228
+ setWalletAddress(nextAddress, { skipAnalytics: shouldSkipAnalytics });
229
+ hasTrackedAccountAddress = true;
230
+ } else if (!ChainController.state.activeCaipAddress) {
231
+ resetWalletState();
232
+ }
233
+ }, { immediate: true });
234
+
235
+ watch(() => appKitAccount.value.isConnected, (connected) => {
236
+ if (!connected && !ChainController.state.activeCaipAddress) {
237
+ resetWalletState();
238
+ hasTrackedAccountAddress = false;
239
+ }
240
+ });
241
+
242
+ const applySettings = () => {
243
+ const pageHeader = pageContent.value?.header;
244
+ const headerSettings = (pageHeader && typeof pageHeader === 'object') ? pageHeader
245
+ : (injectedSiteData.value?.header && typeof injectedSiteData.value.header === 'object') ? injectedSiteData.value.header
246
+ : {};
247
+
248
+ const connectLabel = typeof headerSettings.walletButtonLabel === 'string' ? headerSettings.walletButtonLabel.trim() : '';
249
+ const fallbackConnectLabel = pickLabel(
250
+ pageContent.value?.presale?.connectButtonLabel,
251
+ injectedSiteData.value?.shared?.content?.presale?.connectButtonLabel,
252
+ );
253
+ walletConnectLabel.value = connectLabel || fallbackConnectLabel || DEFAULT_CONNECT_LABEL;
254
+ walletLoadingLabel.value = (typeof headerSettings.walletLoadingLabel === 'string' ? headerSettings.walletLoadingLabel.trim() : '') || DEFAULT_LOADING_LABEL;
255
+ walletConnectedLabel.value = typeof headerSettings.walletConnectedLabel === 'string' ? headerSettings.walletConnectedLabel.trim() : '';
256
+ };
257
+
258
+ watch(() => [pageContent.value?.header, injectedSiteData.value?.header], applySettings, { immediate: true });
259
+
260
+ const handleWalletButtonClick = async () => {
261
+ trackEvent('header_wallet_cta_click', { source: 'header' });
262
+ if (typeof window === 'undefined' || !ModalController) return;
263
+ if (isModalBusy.value) return;
264
+ try {
265
+ if (ModalController.state?.open) { ModalController.close(); return; }
266
+ if (!isWalletConnected.value) {
267
+ setPendingWalletConnectContext({ source: 'header', connector: 'appkit' });
268
+ trackFunnelEvent('wallet_connect_initiated', { wallet_provider: 'appkit', source: 'header' });
269
+ }
270
+ await ModalController.open({
271
+ namespace: ChainController?.state?.activeChain,
272
+ ...(isWalletConnected.value ? {} : { view: 'Connect' }),
273
+ });
274
+ } catch (error) {
275
+ if (import.meta.env.DEV) console.error('Header wallet CTA failed', error);
276
+ setPendingWalletConnectContext(null);
277
+ }
278
+ };
279
+
280
+ onMounted(() => {
281
+ if (typeof window === 'undefined') return;
282
+ if (ChainController?.state) {
283
+ if (ChainController.state.activeCaipAddress) {
284
+ setWalletAddress(ChainController.state.activeCaipAddress, { skipAnalytics: true });
285
+ }
286
+ const activeChain = ChainController.state.activeChain;
287
+ if (activeChain && ChainController.state.chains?.get) {
288
+ const accountState = ChainController.state.chains.get(activeChain)?.accountState;
289
+ if (accountState) {
290
+ if (accountState.address || accountState.caipAddress) setWalletAddress(accountState.address || accountState.caipAddress, { skipAnalytics: true });
291
+ if (accountState.profileName) setWalletProfileName(accountState.profileName, { skipAnalytics: true });
292
+ }
293
+ }
294
+ }
295
+ emitWalletConnectedAnalytics();
296
+ isModalBusy.value = Boolean(ModalController?.state?.loading);
297
+ if (!caipAddressUnsubscribe && ChainController?.subscribeKey) {
298
+ caipAddressUnsubscribe = ChainController.subscribeKey('activeCaipAddress', (next) => {
299
+ if (typeof next === 'string' && next.trim()) setWalletAddress(next);
300
+ else resetWalletState();
301
+ });
302
+ }
303
+ if (!profileNameUnsubscribe && ChainController?.subscribeAccountStateProp) {
304
+ profileNameUnsubscribe = ChainController.subscribeAccountStateProp('profileName', (next) => setWalletProfileName(next, { skipAnalytics: true }));
305
+ }
306
+ if (!modalUnsubscribe && ModalController?.subscribe) {
307
+ modalUnsubscribe = ModalController.subscribe((state) => { isModalBusy.value = Boolean(state.loading); });
308
+ }
309
+ if (!connectedWalletInfoUnsubscribe && ChainController?.subscribeAccountStateProp) {
310
+ connectedWalletInfoUnsubscribe = ChainController.subscribeAccountStateProp('connectedWalletInfo', () => {
311
+ if (isWalletConnected.value) emitWalletConnectedAnalytics();
312
+ else resetWalletState();
313
+ });
314
+ }
315
+ });
316
+
317
+ onBeforeUnmount(() => {
318
+ if (caipAddressUnsubscribe) { caipAddressUnsubscribe(); caipAddressUnsubscribe = undefined; }
319
+ if (profileNameUnsubscribe) { profileNameUnsubscribe(); profileNameUnsubscribe = undefined; }
320
+ if (modalUnsubscribe) { modalUnsubscribe(); modalUnsubscribe = undefined; }
321
+ if (connectedWalletInfoUnsubscribe) { connectedWalletInfoUnsubscribe(); connectedWalletInfoUnsubscribe = undefined; }
322
+ });
323
+ </script>
324
+
325
+ <style scoped>
326
+ .header-wallet {
327
+ display: inline-flex;
328
+ align-items: center;
329
+ flex: 0 1 auto;
330
+ min-width: 0;
331
+ }
332
+
333
+ .header-wallet__button {
334
+ position: relative;
335
+ display: inline-flex;
336
+ align-items: center;
337
+ gap: 0.75rem;
338
+ padding: 0.625rem 1.75rem;
339
+ min-height: 44px;
340
+ letter-spacing: 0.14em;
341
+ font-size: 0.72rem;
342
+ min-width: 0;
343
+ flex: 0 1 auto;
344
+ }
345
+
346
+ .header-wallet__button--loading { cursor: progress; }
347
+ .header-wallet__button--connected { letter-spacing: 0.04em; }
348
+ .header-wallet__button--connected .header-wallet__label { text-transform: none; font-size: 0.78rem; }
349
+
350
+ .header-wallet__icon {
351
+ display: inline-flex;
352
+ align-items: center;
353
+ justify-content: center;
354
+ width: 20px;
355
+ height: 20px;
356
+ color: currentColor;
357
+ }
358
+
359
+ .header-wallet__icon svg { width: 100%; height: 100%; }
360
+
361
+ .header-wallet__label {
362
+ display: inline-flex;
363
+ align-items: center;
364
+ gap: 0.5rem;
365
+ white-space: nowrap;
366
+ overflow: hidden;
367
+ text-overflow: ellipsis;
368
+ max-width: clamp(120px, 32vw, 220px);
369
+ }
370
+
371
+ .header-wallet__spinner {
372
+ width: 18px;
373
+ height: 18px;
374
+ border: 2px solid color-mix(in srgb, currentColor 65%, transparent);
375
+ border-top-color: color-mix(in srgb, var(--brand-header-bg, #04060c) 85%, currentColor 15%);
376
+ border-radius: 50%;
377
+ animation: headerWalletSpin 0.9s linear infinite;
378
+ }
379
+
380
+ @media (max-width: 767px) {
381
+ .header-wallet__button { padding: 0.55rem 1.4rem; gap: 0.6rem; }
382
+ }
383
+
384
+ @media (max-width: 520px) {
385
+ .header-wallet__button { padding: 0.45rem 1.05rem; gap: 0.5rem; letter-spacing: 0.08em; font-size: 0.68rem; }
386
+ .header-wallet__icon { width: 18px; height: 18px; }
387
+ .header-wallet__label { max-width: clamp(100px, 38vw, 180px); }
388
+ }
389
+
390
+ @media (prefers-reduced-motion: reduce) {
391
+ .header-wallet__button { transition: none; }
392
+ .header-wallet__spinner { animation: none; }
393
+ }
394
+
395
+ @keyframes headerWalletSpin {
396
+ to { transform: rotate(360deg); }
397
+ }
398
+ </style>