ultimate-jekyll-manager 0.0.116 → 0.0.118

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.
@@ -1,6 +1,7 @@
1
1
  // Security section module
2
- import fetch from 'wonderful-fetch';
2
+ import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
3
3
  import { FormManager } from '__main_assets__/js/libs/form-manager.js';
4
+ import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
4
5
 
5
6
  let webManager = null;
6
7
  let firebaseAuth = null;
@@ -11,7 +12,6 @@ let signoutAllForm = null; // FormManager instance for sign out all sessions
11
12
  export function init(wm) {
12
13
  webManager = wm;
13
14
  initializeSigninMethods();
14
- initializeSigninMethodForms();
15
15
  initializeSignoutAllForm();
16
16
  }
17
17
 
@@ -19,9 +19,15 @@ export function init(wm) {
19
19
  export function loadData(account) {
20
20
  if (!account) return;
21
21
 
22
- // Update signin methods
22
+ console.log('[DEBUG] security.js - loadData() called with account:', account);
23
+
24
+ // CRITICAL: Update signin methods BEFORE initializing FormManagers
25
+ // This ensures FormManager stores the correct button state from the start
23
26
  updateSigninMethods();
24
27
 
28
+ // Initialize FormManagers AFTER setting correct button states
29
+ initializeSigninMethodForms();
30
+
25
31
  // Update 2FA status
26
32
  update2FAStatus(account.security?.twoFactor);
27
33
 
@@ -31,6 +37,8 @@ export function loadData(account) {
31
37
 
32
38
  // Initialize signin methods
33
39
  async function initializeSigninMethods() {
40
+ console.log('[DEBUG] security.js - initializeSigninMethods() called');
41
+
34
42
  // Get Firebase auth instance
35
43
  firebaseAuth = webManager.firebaseAuth;
36
44
 
@@ -61,13 +69,23 @@ async function checkRedirectResult() {
61
69
 
62
70
  // Update signin methods display
63
71
  async function updateSigninMethods() {
72
+ console.log('[DEBUG] security.js - updateSigninMethods() called');
73
+
64
74
  // Use Firebase auth directly for most up-to-date provider information
65
75
  const firebaseUser = firebaseAuth?.currentUser;
66
- if (!firebaseUser) return;
76
+ if (!firebaseUser) {
77
+ console.log('[DEBUG] security.js - No firebaseUser, returning');
78
+ return;
79
+ }
67
80
 
68
81
  // Get the formatted user from webManager for consistency, but we'll use firebaseUser for provider data
69
82
  const user = webManager.auth().getUser();
70
- if (!user) return;
83
+ if (!user) {
84
+ console.log('[DEBUG] security.js - No user from webManager, returning');
85
+ return;
86
+ }
87
+
88
+ console.log('[DEBUG] security.js - firebaseUser.providerData:', firebaseUser.providerData);
71
89
 
72
90
  // Update password email display
73
91
  const $passwordEmail = document.getElementById('password-email');
@@ -75,42 +93,43 @@ async function updateSigninMethods() {
75
93
  // Check if user has password provider using firebaseUser for most up-to-date data
76
94
  const hasPassword = firebaseUser.providerData?.some(provider => provider.providerId === 'password');
77
95
  $passwordEmail.textContent = hasPassword ? user.email : 'Not set';
96
+ console.log('[DEBUG] security.js - hasPassword:', hasPassword);
78
97
  }
79
98
 
80
99
  // Update Google signin display
81
100
  const $googleEmail = document.getElementById('google-email');
82
101
  const $googleForm = document.getElementById('signin-method-google-form');
83
- const $googleBtn = $googleForm?.querySelector('button[type="submit"]');
84
- const $googleBtnText = $googleBtn?.querySelector('.button-text');
85
- const $googleAction = $googleForm?.querySelector('input[name="action"]');
86
- const $googleIcon = $googleBtn?.querySelector('.fa-icon');
102
+ const $connectButton = $googleForm?.querySelector('button[data-action="connect"]');
103
+ const $disconnectButton = $googleForm?.querySelector('button[data-action="disconnect"]');
87
104
 
88
- if ($googleEmail && $googleBtn) {
105
+ console.log('[DEBUG] security.js - Google DOM elements:', {
106
+ $googleEmail: !!$googleEmail,
107
+ $googleForm: !!$googleForm,
108
+ $connectButton: !!$connectButton,
109
+ $disconnectButton: !!$disconnectButton
110
+ });
111
+
112
+ if ($googleEmail && $connectButton && $disconnectButton) {
89
113
  // Check if user has Google provider using firebaseUser for most up-to-date data
90
114
  const googleProvider = firebaseUser.providerData?.find(provider => provider.providerId === 'google.com');
91
115
 
116
+ console.log('[DEBUG] security.js - googleProvider:', googleProvider);
117
+ console.log('[DEBUG] security.js - googleProvider found:', !!googleProvider);
118
+
92
119
  if (googleProvider) {
120
+ console.log('[DEBUG] security.js - Showing disconnect button');
121
+
93
122
  $googleEmail.textContent = googleProvider.email || 'Connected';
94
- if ($googleBtnText) $googleBtnText.textContent = 'Disconnect';
95
- if ($googleAction) $googleAction.value = 'disconnect';
96
- $googleBtn.classList.remove('btn-primary');
97
- $googleBtn.classList.add('btn-outline-danger');
98
- // Update icon from link to unlink
99
- if ($googleIcon) {
100
- $googleIcon.classList.remove('fa-link');
101
- $googleIcon.classList.add('fa-unlink');
102
- }
123
+ // Hide connect button, show disconnect button
124
+ $connectButton.classList.add('d-none');
125
+ $disconnectButton.classList.remove('d-none');
103
126
  } else {
127
+ console.log('[DEBUG] security.js - Showing connect button');
128
+
104
129
  $googleEmail.textContent = 'Not connected';
105
- if ($googleBtnText) $googleBtnText.textContent = 'Connect';
106
- if ($googleAction) $googleAction.value = 'connect';
107
- $googleBtn.classList.remove('btn-outline-danger');
108
- $googleBtn.classList.add('btn-primary');
109
- // Update icon from unlink to link
110
- if ($googleIcon) {
111
- $googleIcon.classList.remove('fa-unlink');
112
- $googleIcon.classList.add('fa-link');
113
- }
130
+ // Show connect button, hide disconnect button
131
+ $connectButton.classList.remove('d-none');
132
+ $disconnectButton.classList.add('d-none');
114
133
  }
115
134
  }
116
135
  }
@@ -167,16 +186,14 @@ async function updateActiveSessions(account) {
167
186
 
168
187
  // Fetch other active sessions from server
169
188
  try {
170
- const token = await webManager.auth().getIdToken();
171
189
  const serverApiURL = webManager.getApiUrl() + '/backend-manager';
172
190
 
173
- const data = await fetch(serverApiURL, {
191
+ const data = await authorizedFetch(serverApiURL, {
174
192
  method: 'POST',
175
193
  timeout: 60000,
176
194
  response: 'json',
177
195
  tries: 2,
178
196
  body: {
179
- authenticationToken: token,
180
197
  command: 'user:get-active-sessions',
181
198
  payload: {
182
199
  // id: 'app',
@@ -184,14 +201,12 @@ async function updateActiveSessions(account) {
184
201
  },
185
202
  });
186
203
 
187
- console.log('Active sessions data from server:', data);
188
-
189
204
  // Process sessions from server response
190
205
  let sessionData = data || {};
191
206
 
192
- // Add fake data if _test_prefill=true is in query string
207
+ // Add fake data if _dev_prefill=true is in query string
193
208
  const urlParams = new URLSearchParams(window.location.search);
194
- if (urlParams.get('_test_prefill') === 'true') {
209
+ if (urlParams.get('_dev_prefill') === 'true') {
195
210
  console.log('Adding fake session data for testing');
196
211
  const fakeSessions = generateFakeSessions();
197
212
  // Merge fake sessions with existing data (fake sessions don't override real ones)
@@ -248,9 +263,9 @@ async function updateActiveSessions(account) {
248
263
  };
249
264
 
250
265
  // Only add if it's different from current session (different IP or timestamp)
251
- if (!sessions[0] ||
252
- (lastSession.ip !== sessions[0].ip ||
253
- lastSession.timestampUNIX !== sessions[0].timestampUNIX)) {
266
+ if (!sessions[0]
267
+ || (lastSession.ip !== sessions[0].ip
268
+ || lastSession.timestampUNIX !== sessions[0].timestampUNIX)) {
254
269
  sessions.push(lastSession);
255
270
  }
256
271
  }
@@ -261,23 +276,24 @@ async function updateActiveSessions(account) {
261
276
  return;
262
277
  }
263
278
 
264
- const sessionHTML = sessions.map(session => {
279
+ const sessionHTML = sessions.map((session, index) => {
265
280
  const deviceName = session.device || 'Unknown Device';
266
281
  const browserName = session.browser || 'Unknown Browser';
267
282
  const location = formatSessionLocation(session);
283
+ const isLast = index === sessions.length - 1;
268
284
 
269
285
  return `
270
- <div class="list-group-item px-0 bg-body-tertiary">
286
+ <div class="px-0 py-3${isLast ? '' : ' border-bottom'}">
271
287
  <div class="d-flex justify-content-between align-items-start">
272
- <div class="d-flex align-items-start">
273
- <div class="me-3 mt-1">
288
+ <div class="d-flex align-items-center">
289
+ <div class="d-flex align-items-center justify-content-center me-3 flex-shrink-0 text-muted">
274
290
  ${getDeviceIcon(session.platform || deviceName)}
275
291
  </div>
276
292
  <div>
277
- <div class="fw-semibold">${deviceName}</div>
278
- <small class="text-muted d-block">${browserName}${session.mobile !== undefined ? ` • ${session.mobile ? 'Mobile' : 'Desktop'}` : ''}</small>
279
- ${location ? `<small class="text-muted d-block">${location}</small>` : ''}
280
- ${session.ip ? `<small class="text-muted d-block">IP: ${session.ip}</small>` : ''}
293
+ <strong>${deviceName}</strong>
294
+ <div class="text-muted small">${browserName}${session.mobile !== undefined ? ` • ${session.mobile ? 'Mobile' : 'Desktop'}` : ''}</div>
295
+ ${location ? `<div class="text-muted small">${location}</div>` : ''}
296
+ ${session.ip ? `<div class="text-muted small">IP: ${session.ip}</div>` : ''}
281
297
  </div>
282
298
  </div>
283
299
  <div class="text-end">
@@ -294,10 +310,14 @@ async function updateActiveSessions(account) {
294
310
 
295
311
  // Initialize FormManager for signin methods
296
312
  function initializeSigninMethodForms() {
313
+ console.log('[DEBUG] security.js - initializeSigninMethodForms() called');
314
+
297
315
  // Initialize password form
298
316
  const $passwordForm = document.getElementById('signin-method-password-form');
299
317
 
300
318
  if ($passwordForm && !signinMethodForms.has('password')) {
319
+ console.log('[DEBUG] security.js - Initializing password FormManager');
320
+
301
321
  const formManager = new FormManager($passwordForm, {
302
322
  allowMultipleSubmissions: false,
303
323
  autoDisable: true,
@@ -321,42 +341,43 @@ function initializeSigninMethodForms() {
321
341
  const $googleForm = document.getElementById('signin-method-google-form');
322
342
 
323
343
  if ($googleForm && !signinMethodForms.has('google')) {
344
+ console.log('[DEBUG] security.js - About to initialize Google FormManager');
345
+ console.log('[DEBUG] security.js - Google form exists:', !!$googleForm);
346
+
324
347
  const formManager = new FormManager($googleForm, {
325
348
  autoDisable: true,
326
349
  showSpinner: true
327
350
  });
328
351
 
329
352
  signinMethodForms.set('google', formManager);
353
+ console.log('[DEBUG] security.js - Google FormManager initialized and stored');
330
354
 
331
355
  formManager.addEventListener('submit', async (event) => {
332
356
  event.preventDefault();
333
- const { data } = event.detail;
357
+ const { submitButton } = event.detail;
358
+
359
+ // Determine action from the clicked button's data-action attribute
360
+ const action = submitButton?.getAttribute('data-action');
334
361
 
335
362
  try {
336
- if (data.action === 'disconnect') {
363
+ if (action === 'disconnect') {
337
364
  await disconnectGoogleProvider();
338
- } else {
365
+ } else if (action === 'connect') {
339
366
  await connectGoogleProvider();
340
367
  }
341
368
 
342
369
  // Set form state back to ready first
343
370
  formManager.setFormState('ready');
344
371
 
345
- // Then update display (after FormManager has restored button)
346
- // Use setTimeout to ensure FormManager has finished updating
347
- setTimeout(() => {
348
- updateSigninMethods();
349
- }, 0);
372
+ // Then update display (this will set the button text correctly again)
373
+ updateSigninMethods();
350
374
  } catch (error) {
351
375
  // Reset form state
352
376
  formManager.setFormState('ready');
353
377
 
354
378
  // If user cancelled, also update the display to ensure button state is correct
355
379
  if (error.message === 'Disconnection cancelled') {
356
- // Update display to ensure button reflects current state
357
- setTimeout(() => {
358
- updateSigninMethods();
359
- }, 0);
380
+ updateSigninMethods();
360
381
  } else {
361
382
  // Show error for other failures
362
383
  formManager.showError(error);
@@ -429,9 +450,9 @@ async function connectGoogleProvider() {
429
450
  return result;
430
451
  } catch (error) {
431
452
  // Check if we should fallback to redirect
432
- if (error.code === 'auth/popup-blocked' ||
433
- error.code === 'auth/popup-closed-by-user' ||
434
- error.code === 'auth/cancelled-popup-request') {
453
+ if (error.code === 'auth/popup-blocked'
454
+ || error.code === 'auth/popup-closed-by-user'
455
+ || error.code === 'auth/cancelled-popup-request') {
435
456
 
436
457
  console.log('Popup failed, falling back to redirect:', error.code);
437
458
 
@@ -583,20 +604,25 @@ function getPlatformName(platform) {
583
604
  // Get device icon based on device type
584
605
  function getDeviceIcon(device) {
585
606
  const deviceLower = (device || '').toLowerCase();
607
+ let iconName = 'desktop'; // default
586
608
 
587
- if (deviceLower.includes('iphone') || deviceLower.includes('ipad') || deviceLower.includes('ios') || deviceLower.includes('mac')) {
588
- return '<i class="fa-brands fa-apple fa-lg"></i>';
609
+ if (deviceLower.includes('iphone')
610
+ || deviceLower.includes('ipad')
611
+ || deviceLower.includes('ios')
612
+ || deviceLower.includes('mac')) {
613
+ iconName = 'apple';
589
614
  } else if (deviceLower.includes('android')) {
590
- return '<i class="fa-brands fa-android fa-lg"></i>';
615
+ iconName = 'android';
591
616
  } else if (deviceLower.includes('windows')) {
592
- return '<i class="fa-brands fa-windows fa-lg"></i>';
617
+ iconName = 'windows';
593
618
  } else if (deviceLower.includes('linux')) {
594
- return '<i class="fa-brands fa-linux fa-lg"></i>';
619
+ iconName = 'linux';
595
620
  } else if (deviceLower.includes('chrome')) {
596
- return '<i class="fa-brands fa-chrome fa-lg"></i>';
597
- } else {
598
- return '<i class="fa-solid fa-desktop fa-lg"></i>';
621
+ iconName = 'chrome';
599
622
  }
623
+
624
+ // Get the pre-rendered icon
625
+ return getPrerenderedIcon(iconName);
600
626
  }
601
627
 
602
628
  // Format location from session data
@@ -703,4 +729,3 @@ function formatDate(timestamp) {
703
729
  // More than 7 days - show full date
704
730
  return date.toLocaleDateString() + ' at ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
705
731
  }
706
-
@@ -298,8 +298,8 @@ async function initializeCheckout() {
298
298
  const urlParams = new URLSearchParams(window.location.search);
299
299
  const productId = urlParams.get('product');
300
300
  const frequency = urlParams.get('frequency') || 'annually';
301
- const _test_appId = urlParams.get('_test_appId');
302
- const _test_trialEligible = urlParams.get('_test_trialEligible');
301
+ const _dev_appId = urlParams.get('_dev_appId');
302
+ const _dev_trialEligible = urlParams.get('_dev_trialEligible');
303
303
 
304
304
  // Product ID is required
305
305
  if (!productId) {
@@ -307,7 +307,7 @@ async function initializeCheckout() {
307
307
  }
308
308
 
309
309
  // Check for testing parameters
310
- const appId = _test_appId || webManager.config.brand.id;
310
+ const appId = _dev_appId || webManager.config.brand.id;
311
311
 
312
312
  // Warmup server (fire and forget)
313
313
  warmupServer(webManager);
@@ -331,10 +331,10 @@ async function initializeCheckout() {
331
331
  let trialEligibilityResult = trialEligible;
332
332
 
333
333
  // Override trial eligibility for testing (only in development)
334
- if (_test_trialEligible && webManager.isDevelopment()) {
335
- if (_test_trialEligible === 'false') {
334
+ if (_dev_trialEligible && webManager.isDevelopment()) {
335
+ if (_dev_trialEligible === 'false') {
336
336
  trialEligibilityResult = { status: 'fulfilled', value: false };
337
- } else if (_test_trialEligible === 'true') {
337
+ } else if (_dev_trialEligible === 'true') {
338
338
  trialEligibilityResult = { status: 'fulfilled', value: true };
339
339
  }
340
340
  }
@@ -41,9 +41,9 @@ export class PaymentProcessorManager {
41
41
 
42
42
  // Determine processor based on payment method and available API keys
43
43
  if (paymentMethod === 'card') {
44
- // Check for _test_cardProcessor override in URL params (for testing)
44
+ // Check for _dev_cardProcessor override in URL params (for testing)
45
45
  const urlParams = new URLSearchParams(window.location.search);
46
- const forcedProcessor = urlParams.get('_test_cardProcessor');
46
+ const forcedProcessor = urlParams.get('_dev_cardProcessor');
47
47
 
48
48
  if (forcedProcessor && this.processors[forcedProcessor]) {
49
49
  processorName = forcedProcessor;
@@ -30,7 +30,7 @@ export function buildPaymentIntentData(webManager) {
30
30
  let processorName = state.paymentMethod;
31
31
  if (state.paymentMethod === 'card') {
32
32
  // Determine which processor will be used for card payments
33
- const forcedProcessor = urlParams.get('_test_cardProcessor');
33
+ const forcedProcessor = urlParams.get('_dev_cardProcessor');
34
34
 
35
35
  if (forcedProcessor) {
36
36
  processorName = forcedProcessor;
@@ -46,7 +46,7 @@ export function buildPaymentIntentData(webManager) {
46
46
  // Get UTM parameters from storage
47
47
  const utmData = webManager.storage().get('marketing.utm');
48
48
  let utm = {};
49
-
49
+
50
50
  // Check if stored UTM data exists and is less than 30 days old
51
51
  if (utmData && utmData.timestamp && utmData.tags) {
52
52
  const daysDiff = (new Date() - new Date(utmData.timestamp)) / (1000 * 60 * 60 * 24);
@@ -56,8 +56,8 @@ export function buildPaymentIntentData(webManager) {
56
56
  }
57
57
 
58
58
  // Check for test app ID override
59
- const _test_appId = urlParams.get('_test_appId');
60
- const appId = _test_appId || webManager.config.brand.id;
59
+ const _dev_appId = urlParams.get('_dev_appId');
60
+ const appId = _dev_appId || webManager.config.brand.id;
61
61
 
62
62
  // Build the payment intent data structure
63
63
  const paymentIntentData = {
@@ -35,6 +35,29 @@
35
35
  </div>
36
36
  </div>
37
37
 
38
+ {%- comment -%}
39
+ Icon Pre-rendering System
40
+ Pages can specify which icons to pre-render in their frontmatter:
41
+ prerender_icons:
42
+ - name: "apple"
43
+ class: "fa-3xl"
44
+ - name: "android"
45
+ class: "fa-2xl"
46
+ {%- endcomment -%}
47
+ {%- assign icons = page.resolved.prerender_icons | default: empty -%}
48
+ {%- iftruthy icons -%}
49
+ <!-- Pre-rendered Icon Templates -->
50
+ <div id="prerendered-icons" class="d-none" aria-hidden="true">
51
+ {%- for icon in icons -%}
52
+ {%- assign icon_name = icon.name | default: icon -%}
53
+ {%- assign icon_class = icon.class | default: "fa-3xl" -%}
54
+ <div data-icon="{{ icon_name }}" data-class="{{ icon_class }}">
55
+ {% uj_icon icon_name, icon_class %}
56
+ </div>
57
+ {%- endfor -%}
58
+ </div>
59
+ {%- endiftruthy -%}
60
+
38
61
  <!-- Script to prevent clicks on disabled elements during page load -->
39
62
  <script type="text/javascript">
40
63
  (function() {
@@ -13,6 +13,21 @@ web_manager:
13
13
  config:
14
14
  policy: "authenticated"
15
15
 
16
+ ### ICON PRE-RENDERING ###
17
+ prerender_icons:
18
+ - name: "apple"
19
+ class: "fa-3xl"
20
+ - name: "android"
21
+ class: "fa-3xl"
22
+ - name: "windows"
23
+ class: "fa-3xl"
24
+ - name: "linux"
25
+ class: "fa-3xl"
26
+ - name: "chrome"
27
+ class: "fa-3xl"
28
+ - name: "desktop"
29
+ class: "fa-3xl"
30
+
16
31
  ### PAGE CONFIG ###
17
32
  sections:
18
33
  - id: "profile"
@@ -790,20 +805,24 @@ badges:
790
805
  </div>
791
806
  </div>
792
807
  <div class="flex-shrink-0">
793
- <form id="signin-method-{{ method.id }}-form" class="d-inline" novalidate>
808
+ <form id="signin-method-{{ method.id }}-form" class="d-grid d-sm-inline-block" novalidate>
794
809
  <input type="hidden" name="method" value="{{ method.id }}">
795
810
  {% if method.id == "password" %}
796
- <input type="hidden" name="action" value="change">
797
- <button type="submit" class="btn btn-primary btn-sm w-100 w-sm-auto">
811
+ <button type="submit" class="btn btn-primary btn-sm" data-action="change">
798
812
  {% uj_icon "key", "me-1" %}
799
813
  <span class="button-text">Change</span>
800
814
  </button>
801
815
  {% else %}
802
- <input type="hidden" name="action" value="connect">
803
- <button type="submit" class="btn btn-primary btn-sm w-100 w-sm-auto">
816
+ <!-- Connect button -->
817
+ <button type="submit" class="btn btn-primary btn-sm" data-action="connect">
804
818
  {% uj_icon "link", "me-1" %}
805
819
  <span class="button-text">Connect</span>
806
820
  </button>
821
+ <!-- Disconnect button (hidden by default) -->
822
+ <button type="submit" class="btn btn-sm btn-outline-danger d-none" data-action="disconnect">
823
+ {% uj_icon "unlink", "me-1" %}
824
+ <span class="button-text">Disconnect</span>
825
+ </button>
807
826
  {% endif %}
808
827
  </form>
809
828
  </div>
@@ -828,6 +847,7 @@ badges:
828
847
  <div class="card-body">
829
848
  <h5 class="card-title">Active Sessions</h5>
830
849
  <p class="card-text text-muted">Manage your active sessions across devices. Review and revoke access from unrecognized devices.</p>
850
+
831
851
  <div id="active-sessions-list" class="list-group list-group-flush mb-3">
832
852
  <!-- Sessions will be loaded here -->
833
853
  <div class="text-center py-3">
@@ -872,7 +892,7 @@ badges:
872
892
  </div>
873
893
 
874
894
  {% for connection in page.resolved.connections %}
875
- <div id="connection-{{ connection.id }}" class="list-group-item border-bottom-0 {% unless forloop.first %}border-top{% endunless %} px-0 py-3 d-none bg-body-tertiary">
895
+ <div id="connection-{{ connection.id }}" class="{% unless forloop.first %}border-top{% endunless %} px-0 py-3 d-none">
876
896
  <div class="d-flex flex-column flex-sm-row align-items-stretch align-items-sm-center justify-content-between gap-3">
877
897
  <div class="d-flex align-items-center">
878
898
  <div class="d-flex align-items-center justify-content-center me-3 flex-shrink-0 fa fa-3xl">
@@ -884,15 +904,22 @@ badges:
884
904
  </div>
885
905
  </div>
886
906
  <div class="text-start text-sm-end flex-shrink-0">
887
- <form id="connection-form-{{ connection.id }}" class="d-inline" novalidate>
888
- <input type="hidden" name="provider" value="{{ connection.id }}">
889
- <input type="hidden" name="action" value="connect">
890
- <button type="submit" class="btn btn-sm btn-primary w-100 w-sm-auto" data-connect-text="Connect" data-disconnect-text="Disconnect">
891
- {% uj_icon "link", "fa-sm" %}
892
- <span class="button-text">Connect</span>
893
- </button>
894
- </form>
895
- <small class="text-muted d-block mt-1" id="{{ connection.id }}-connection-status"></small>
907
+ <div>
908
+ <form id="connection-form-{{ connection.id }}" class="d-grid d-sm-inline-block" novalidate>
909
+ <input type="hidden" name="provider" value="{{ connection.id }}">
910
+ <!-- Connect button -->
911
+ <button type="submit" class="btn btn-sm btn-primary" data-action="connect">
912
+ {% uj_icon "link", "fa-sm" %}
913
+ <span class="button-text">Connect</span>
914
+ </button>
915
+ <!-- Disconnect button (hidden by default) -->
916
+ <button type="submit" class="btn btn-sm btn-outline-danger d-none" data-action="disconnect">
917
+ {% uj_icon "unlink", "fa-sm" %}
918
+ <span class="button-text">Disconnect</span>
919
+ </button>
920
+ </form>
921
+ <small class="text-muted d-block mt-1" id="{{ connection.id }}-connection-status"></small>
922
+ </div>
896
923
  </div>
897
924
  </div>
898
925
  </div>
@@ -0,0 +1,7 @@
1
+ ---
2
+ ### ALL PAGES ###
3
+ layout: blueprint/pricing
4
+ permalink: /pricing
5
+
6
+ ### REGULAR PAGES ###
7
+ ---