@fleetbase/registry-bridge-engine 0.0.10 → 0.0.12

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.
@@ -6,6 +6,11 @@
6
6
  <div class="flex flex-col">
7
7
  <div class="font-semibold text-sm block {{@nameTextClass}}">{{@extension.name}}</div>
8
8
  <div class="text-xs {{@descriptionTextClass}}">{{n-a @extension.description}}</div>
9
+ {{#if @extension.payment_required}}
10
+ <Badge @status="success" @hideStatusDot={{true}}>{{format-currency @extension.price @extension.currency}}</Badge>
11
+ {{else}}
12
+ <Badge @status="success" @hideStatusDot={{true}}>Free</Badge>
13
+ {{/if}}
9
14
  </div>
10
15
  {{yield @extension}}
11
16
  </div>
@@ -40,9 +40,21 @@ export default class ExtensionCardComponent extends Component {
40
40
 
41
41
  @action onClick(options = {}) {
42
42
  const installChannel = `install.${this.currentUser.companyId}.${this.extension.id}`;
43
+ const isAuthor = this.extension.is_author === true;
44
+ const isSelfManaged = this.extension.self_managed === true;
43
45
  const isAlreadyPurchased = this.extension.is_purchased === true;
44
46
  const isAlreadyInstalled = this.extension.is_installed === true;
45
- const isPaymentRequired = this.extension.payment_required === true && isAlreadyPurchased === false;
47
+ const isPaymentRequired = !isAuthor && this.extension.payment_required === true && isAlreadyPurchased === false;
48
+ const goBack = async (modal) => {
49
+ await modal.done();
50
+ later(
51
+ this,
52
+ () => {
53
+ this.onClick();
54
+ },
55
+ 100
56
+ );
57
+ };
46
58
 
47
59
  if (typeof this.args.onClick === 'function') {
48
60
  this.args.onClick(this.extension);
@@ -63,6 +75,13 @@ export default class ExtensionCardComponent extends Component {
63
75
  stepDescription: 'Awaiting install to begin...',
64
76
  progress: 0,
65
77
  extension: this.extension,
78
+ viewSelfManagesInstallInstructions: () => {
79
+ this.selfManagedInstallInstructions({
80
+ extension: this.extension,
81
+ confirm: goBack,
82
+ decline: goBack,
83
+ });
84
+ },
66
85
  confirm: async (modal) => {
67
86
  modal.startLoading();
68
87
 
@@ -71,6 +90,22 @@ export default class ExtensionCardComponent extends Component {
71
90
  return this.startCheckoutSession();
72
91
  }
73
92
 
93
+ // If self managed just prompt instructions
94
+ if (isSelfManaged) {
95
+ await modal.done();
96
+ return later(
97
+ this,
98
+ () => {
99
+ return this.selfManagedInstallInstructions({
100
+ extension: this.extension,
101
+ confirm: goBack,
102
+ decline: goBack,
103
+ });
104
+ },
105
+ 100
106
+ );
107
+ }
108
+
74
109
  // Listen for install progress
75
110
  this.socket.listen(installChannel, ({ process, step, progress }) => {
76
111
  let stepDescription;
@@ -121,6 +156,16 @@ export default class ExtensionCardComponent extends Component {
121
156
  });
122
157
  }
123
158
 
159
+ async selfManagedInstallInstructions(options = {}) {
160
+ await this.modalsManager.done();
161
+ this.modalsManager.show('modals/self-managed-install-instructions', {
162
+ title: 'Install a Self Managed Extension',
163
+ hideDeclineButton: true,
164
+ acceptButtonText: 'Done',
165
+ ...options,
166
+ });
167
+ }
168
+
124
169
  async startCheckoutSession() {
125
170
  const checkout = await this.stripe.initEmbeddedCheckout({
126
171
  fetchClientSecret: this.fetchClientSecret.bind(this),
@@ -69,7 +69,6 @@
69
69
 
70
70
  <InputGroup
71
71
  @name={{t "registry-bridge.developers.extensions.extension-form.extension-tags"}}
72
- @wrapperClass="mb-0i"
73
72
  @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-tags-help-text"}}
74
73
  >
75
74
  <TagInput
@@ -85,6 +84,19 @@
85
84
  {{tag}}
86
85
  </TagInput>
87
86
  </InputGroup>
87
+
88
+ <InputGroup @wrapperClass="mb-0i">
89
+ <Toggle
90
+ @isToggled={{@extension.self_managed}}
91
+ @onToggle={{fn (mut @extension.self_managed)}}
92
+ @helpText={{t "registry-bridge.developers.extensions.extension-form.self-managed-help-text"}}
93
+ >
94
+ <span class="dark:text-gray-100 text-sm mx-2">{{t "registry-bridge.developers.extensions.extension-form.self-managed"}}</span>
95
+ </Toggle>
96
+ <p class="mt-2 text-xs bg-blue-800 border border-blue-600 px-2 py-2 rounded-md text-blue-100">
97
+ {{t "registry-bridge.developers.extensions.extension-form.self-managed-help-text"}}
98
+ </p>
99
+ </InputGroup>
88
100
  </ContentPanel>
89
101
  <ContentPanel @title={{t "registry-bridge.developers.extensions.extension-form.extension-bundle"}} @open={{true}} @pad={{false}} @panelBodyClass="bg-white dark:bg-gray-800">
90
102
  <div class="px-4 pb-4 pt-3 flex flex-col flex-grow-0">
@@ -101,7 +113,13 @@
101
113
  {{/if}}
102
114
  </div>
103
115
  </ContentPanel>
104
- <ContentPanel @title={{t "registry-bridge.developers.extensions.extension-form.extension-listing-details"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
116
+ <ContentPanel
117
+ @title={{t "registry-bridge.developers.extensions.extension-form.extension-listing-details"}}
118
+ @open={{true}}
119
+ @pad={{true}}
120
+ @panelBodyClass="bg-white dark:bg-gray-800"
121
+ @actionButtons={{this.listingDetailsPanelActions}}
122
+ >
105
123
  <InputGroup
106
124
  @name={{t "registry-bridge.developers.extensions.extension-form.extension-promotional-text"}}
107
125
  @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-promotional-text-help-text"}}
@@ -3,6 +3,8 @@ import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
4
  import { action } from '@ember/object';
5
5
  import { task } from 'ember-concurrency';
6
+ import { later } from '@ember/runloop';
7
+ import formatCurrency from '@fleetbase/ember-ui/utils/format-currency';
6
8
 
7
9
  export default class ExtensionFormComponent extends Component {
8
10
  @service store;
@@ -13,6 +15,14 @@ export default class ExtensionFormComponent extends Component {
13
15
  @tracked subscriptionModelOptions = ['flat_rate', 'tiered', 'usage'];
14
16
  @tracked billingPeriodOptions = ['daily', 'weekly', 'monthly', 'quarterly', 'yearly'];
15
17
  @tracked uploadQueue = [];
18
+ listingDetailsPanelActions = [
19
+ {
20
+ type: 'link',
21
+ size: 'xs',
22
+ text: 'Preview Listing',
23
+ onClick: this.previewListing,
24
+ },
25
+ ];
16
26
  acceptedImageTypes = ['image/jpeg', 'image/png', 'image/gif'];
17
27
  acceptedBundleTypes = [
18
28
  'application/zip',
@@ -120,4 +130,51 @@ export default class ExtensionFormComponent extends Component {
120
130
  @action removeFile(file) {
121
131
  return file.destroyRecord();
122
132
  }
133
+
134
+ @action previewListing(options = {}) {
135
+ const extension = this.args.extension;
136
+ const isAlreadyPurchased = extension.is_purchased === true;
137
+ const isAlreadyInstalled = extension.is_installed === true;
138
+ const isPaymentRequired = extension.payment_required === true && isAlreadyPurchased === false;
139
+ const goBack = async (modal) => {
140
+ await modal.done();
141
+ later(
142
+ this,
143
+ () => {
144
+ this.previewListing();
145
+ },
146
+ 100
147
+ );
148
+ };
149
+
150
+ this.modalsManager.show('modals/extension-details', {
151
+ titleComponent: 'extension-modal-title',
152
+ modalClass: 'flb--extension-modal modal-lg',
153
+ modalHeaderClass: 'flb--extension-modal-header',
154
+ acceptButtonText: isPaymentRequired ? `Purchase for ${formatCurrency(extension.price, extension.currency)}` : isAlreadyInstalled ? 'Installed' : 'Install',
155
+ acceptButtonIcon: isPaymentRequired ? 'credit-card' : isAlreadyInstalled ? 'check' : 'download',
156
+ acceptButtonDisabled: true,
157
+ acceptButtonScheme: isPaymentRequired ? 'success' : 'primary',
158
+ declineButtonText: 'Done',
159
+ viewSelfManagesInstallInstructions: () => {
160
+ this.selfManagedInstallInstructions({
161
+ extension,
162
+ confirm: goBack,
163
+ decline: goBack,
164
+ });
165
+ },
166
+ extension,
167
+ ...options,
168
+ });
169
+ }
170
+
171
+ async selfManagedInstallInstructions(options = {}) {
172
+ await this.modalsManager.done();
173
+ this.modalsManager.show('modals/self-managed-install-instructions', {
174
+ title: 'Install a Self Managed Extension',
175
+ hideDeclineButton: true,
176
+ acceptButtonText: 'Done',
177
+ ...options,
178
+ });
179
+ }
123
180
  }
@@ -1,14 +1,20 @@
1
1
  <ContentPanel @title={{t "registry-bridge.developers.extensions.extension-form.extension-payment-details"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
2
- <InputGroup @wrapperClass={{unless @extension.payment_required "mb-0i"}}>
3
- <Toggle
4
- @isToggled={{@extension.payment_required}}
5
- @onToggle={{fn (mut @extension.payment_required)}}
6
- @label={{t "registry-bridge.developers.extensions.extension-form.extension-payment-required"}}
7
- @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-payment-required-help-text"}}
8
- />
9
- </InputGroup>
10
- {{#if @extension.payment_required}}
11
- {{!-- <InputGroup>
2
+ {{#if this.lookupStripeConnectAccount.isRunning}}
3
+ <div class="flex items-center justify-center flex-1 p-4">
4
+ <Spinner @loadingMessage="Loading monetization settings..." @loadingMessageClass="ml-2 text-black dark:text-white" @wrapperClass="flex flex-row items-center" />
5
+ </div>
6
+ {{else}}
7
+ {{#if this.hasStripeConnectAccount}}
8
+ <InputGroup @wrapperClass={{unless @extension.payment_required "mb-0i"}}>
9
+ <Toggle
10
+ @isToggled={{@extension.payment_required}}
11
+ @onToggle={{fn (mut @extension.payment_required)}}
12
+ @label={{t "registry-bridge.developers.extensions.extension-form.extension-payment-required"}}
13
+ @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-payment-required-help-text"}}
14
+ />
15
+ </InputGroup>
16
+ {{#if @extension.payment_required}}
17
+ {{!-- <InputGroup>
12
18
  <Toggle
13
19
  @isToggled={{@extension.subscription_required}}
14
20
  @onToggle={{fn (mut @extension.subscription_required)}}
@@ -16,40 +22,54 @@
16
22
  @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-required-help-text"}}
17
23
  />
18
24
  </InputGroup> --}}
19
- {{#if @extension.subscription_required}}
20
- <InputGroup
21
- @name={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-billing-period"}}
22
- @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-billing-period-help-text"}}
23
- >
24
- <Select
25
- @value={{@extension.subscription_billing_period}}
26
- @options={{this.billingPeriodOptions}}
27
- @onSelect={{fn (mut @extension.subscription_billing_period)}}
28
- @placeholder={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-billing-period-placeholder"}}
29
- class="w-full"
30
- />
31
- </InputGroup>
32
- <InputGroup
33
- @name={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-amount"}}
34
- @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-amount-help-text"}}
35
- @wrapperClass="mb-0i"
36
- >
37
- <MoneyInput @value={{@extension.subscription_amount}} @currency="USD" />
38
- </InputGroup>
25
+ {{#if @extension.subscription_required}}
26
+ <InputGroup
27
+ @name={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-billing-period"}}
28
+ @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-billing-period-help-text"}}
29
+ >
30
+ <Select
31
+ @value={{@extension.subscription_billing_period}}
32
+ @options={{this.billingPeriodOptions}}
33
+ @onSelect={{fn (mut @extension.subscription_billing_period)}}
34
+ @placeholder={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-billing-period-placeholder"}}
35
+ class="w-full"
36
+ />
37
+ </InputGroup>
38
+ <InputGroup
39
+ @name={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-amount"}}
40
+ @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-subscription-amount-help-text"}}
41
+ @wrapperClass="mb-0i"
42
+ >
43
+ <MoneyInput @value={{@extension.subscription_amount}} @currency="USD" />
44
+ </InputGroup>
45
+ {{else}}
46
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-2">
47
+ <InputGroup
48
+ @name={{t "registry-bridge.developers.extensions.extension-form.extension-price"}}
49
+ @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-price-help-text"}}
50
+ >
51
+ <MoneyInput @value={{@extension.price}} @currency="USD" />
52
+ </InputGroup>
53
+ <InputGroup
54
+ @name={{t "registry-bridge.developers.extensions.extension-form.extension-sale-price"}}
55
+ @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-sale-price-help-text"}}
56
+ >
57
+ <MoneyInput @value={{@extension.sale_price}} @currency="USD" />
58
+ </InputGroup>
59
+ </div>
60
+ {{/if}}
61
+ {{/if}}
39
62
  {{else}}
40
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-2">
41
- <InputGroup
42
- @name={{t "registry-bridge.developers.extensions.extension-form.extension-price"}}
43
- @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-price-help-text"}}
44
- >
45
- <MoneyInput @value={{@extension.price}} @currency="USD" />
46
- </InputGroup>
47
- <InputGroup
48
- @name={{t "registry-bridge.developers.extensions.extension-form.extension-sale-price"}}
49
- @helpText={{t "registry-bridge.developers.extensions.extension-form.extension-sale-price-help-text"}}
50
- >
51
- <MoneyInput @value={{@extension.sale_price}} @currency="USD" />
52
- </InputGroup>
63
+ <div class="container">
64
+ <div class="max-w-3xl mx-auto mt-4">
65
+ <div class="content">
66
+ <div class="flex flex-col items-center justify-center">
67
+ <h1 class="text-lg font-semibold mb-1">Your account is not setup to accept payments yet.</h1>
68
+ <p class="text-sm mb-2">To accept payments for extensions, you must complete the onboard process via Stripe.</p>
69
+ <Button @type="primary" @size="lg" @text="Start payments onboard" @onClick={{transition-to "developers.payments.onboard"}} />
70
+ </div>
71
+ </div>
72
+ </div>
53
73
  </div>
54
74
  {{/if}}
55
75
  {{/if}}
@@ -1,7 +1,25 @@
1
1
  import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+ import { task } from 'ember-concurrency';
3
5
 
4
6
  export default class ExtensionMonetizeFormComponent extends Component {
7
+ @service fetch;
5
8
  @tracked subscriptionModelOptions = ['flat_rate', 'tiered', 'usage'];
6
9
  @tracked billingPeriodOptions = ['daily', 'weekly', 'monthly', 'quarterly', 'yearly'];
10
+ @tracked hasStripeConnectAccount = false;
11
+
12
+ constructor() {
13
+ super(...arguments);
14
+ this.lookupStripeConnectAccount.perform();
15
+ }
16
+
17
+ @task *lookupStripeConnectAccount() {
18
+ try {
19
+ const { hasStripeConnectAccount } = yield this.fetch.get('payments/has-stripe-connect-account', {}, { namespace: '~registry/v1' });
20
+ this.hasStripeConnectAccount = hasStripeConnectAccount;
21
+ } catch (error) {
22
+ this.hasStripeConnectAccount = false;
23
+ }
24
+ }
7
25
  }
@@ -2,7 +2,7 @@
2
2
  <div class="modal-body-container space-y-3">
3
3
  <div class="flex flex-row items-center space-x-2">
4
4
  <div class="flb--extension-tag shadow-sm border dark:bg-gray-800 dark:border-gray-800 dark:text-gray-200">
5
- Extension
5
+ {{t "registry-bridge.component.extension-details-modal.extension"}}
6
6
  </div>
7
7
  <div class="flb--extension-tag shadow-sm border dark:bg-gray-800 dark:border-gray-800 dark:text-gray-200">
8
8
  {{this.extension.category_name}}
@@ -28,40 +28,59 @@
28
28
  {{/if}}
29
29
  {{/if}}
30
30
  <div>
31
- <h3 class="dark:text-white font-semibold mb-1">Overview</h3>
31
+ <h3 class="dark:text-white font-semibold mb-1">{{t "registry-bridge.component.extension-details-modal.overview"}}</h3>
32
32
  <div class="space-y-1">
33
33
  <p class="dark:text-gray-200 text-sm">{{this.extension.description}}</p>
34
34
  <p class="dark:text-gray-200 text-sm">{{html-safe this.extension.promotional_text}}</p>
35
35
  </div>
36
36
  </div>
37
37
  <div>
38
- <h3 class="dark:text-white font-semibold mb-1">Details</h3>
38
+ <h3 class="dark:text-white font-semibold mb-1">{{t "registry-bridge.component.extension-details-modal.details"}}</h3>
39
39
  <div class="space-y-2">
40
40
  <div class="grid grid-cols-4 gap-2">
41
41
  <div class="space-y-2">
42
42
  <div>
43
- <div class="text-sm font-semibold dark:text-gray-100">Version</div>
44
- <div class="text-sm dark:text-gray-200">{{this.extension.current_bundle_version}}</div>
43
+ <div class="text-sm font-semibold dark:text-gray-100">{{t "registry-bridge.component.extension-details-modal.version"}}</div>
44
+ <div class="text-sm dark:text-gray-200">{{n-a this.extension.current_bundle_version}}</div>
45
45
  </div>
46
46
  <div>
47
- <div class="text-sm font-semibold dark:text-gray-100">Updated</div>
48
- <div class="text-sm dark:text-gray-200">{{this.extension.updatedAt}}</div>
47
+ <div class="text-sm font-semibold dark:text-gray-100">{{t "registry-bridge.component.extension-details-modal.updated"}}</div>
48
+ <div class="text-sm dark:text-gray-200">{{n-a this.extension.updatedAt}}</div>
49
49
  </div>
50
50
  </div>
51
51
  <div class="space-y-2">
52
52
  <div>
53
- <div class="text-sm font-semibold dark:text-gray-100">Author</div>
54
- <div class="text-sm dark:text-gray-200">{{this.extension.publisher_name}}</div>
53
+ <div class="text-sm font-semibold dark:text-gray-100">{{t "registry-bridge.component.extension-details-modal.author"}}</div>
54
+ <div class="text-sm dark:text-gray-200">{{n-a this.extension.publisher_name}}</div>
55
55
  </div>
56
56
  <div>
57
- <div class="text-sm font-semibold dark:text-gray-100">Website</div>
58
- <a
59
- class="text-sm dark:text-gray-200"
60
- href={{this.extension.website_url}}
61
- target={{dasherize (concat this.extension.name "-website")}}
62
- >{{this.extension.website_url}}</a>
57
+ <div class="text-sm font-semibold dark:text-gray-100">{{t "registry-bridge.component.extension-details-modal.website"}}</div>
58
+ {{#if this.extension.website_url}}
59
+ <a
60
+ class="text-sm text-blue-400 hover:text-blue-300"
61
+ href={{this.extension.website_url}}
62
+ target={{dasherize (concat this.extension.name "-website")}}
63
+ >{{this.extension.website_url}}</a>
64
+ {{else}}
65
+ <div class="text-sm dark:text-gray-200">-</div>
66
+ {{/if}}
63
67
  </div>
64
68
  </div>
69
+ <div class="space-y-2">
70
+ {{#if this.extension.self_managed}}
71
+ <div>
72
+ <Badge @status="info" @hideStatusDot={{true}} @helpText={{t "registry-bridge.component.extension-details-modal.self-managed-help-text"}}>{{t
73
+ "registry-bridge.component.extension-details-modal.self-managed"
74
+ }}</Badge>
75
+ {{#if @options.viewSelfManagesInstallInstructions}}
76
+ <a href="#" class="text-xs text-blue-400 hover:opacity-50" {{on "click" @options.viewSelfManagesInstallInstructions}}><FaIcon
77
+ @icon="circle-info"
78
+ class="mr-1"
79
+ />How to install</a>
80
+ {{/if}}
81
+ </div>
82
+ {{/if}}
83
+ </div>
65
84
  </div>
66
85
  </div>
67
86
  </div>
@@ -0,0 +1,40 @@
1
+ <Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
2
+ <div class="modal-body-container">
3
+ <p class="text-sm mb-4 text-gray-900 dark:text-gray-200">
4
+ Begin the installation from the base directory of your Fleetbase instance. For comprehensive instructions, visit the
5
+ <a href="https://github.com/fleetbase/fleetbase-cli" target="fleetbase-cli" class="text-blue-400 hover:opacity-50">
6
+ <FaIcon @icon="arrow-up-right-from-square" class="text-xs" />
7
+ Fleetbase CLI GitHub page</a>.
8
+ </p>
9
+ <ol class="self-managed-install-instructions">
10
+ <li>
11
+ <div class="flex">
12
+ First,
13
+ <LinkTo @route="console.extensions.developers.credentials" class="ml-1 text-blue-400 hover:opacity-50">generate a Fleetbase registry credentials token.</LinkTo>
14
+ </div>
15
+ </li>
16
+ <li>
17
+ <div class="flex flex-col">
18
+ <div class="mb-2">Install the Fleetbase CLI globally using npm:</div>
19
+ <ClickToCopy @value={{concat "npm i -g @fleetbase/cli"}}><code>npm i -g @fleetbase/cli</code></ClickToCopy>
20
+ </div>
21
+ </li>
22
+ <li>
23
+ <div class="flex flex-col">
24
+ <div class="mb-2">Set your authentication token with the CLI:</div>
25
+ <ClickToCopy @value={{concat "flb set-auth {YOUR_TOKEN}"}}><code>flb set-auth {YOUR_TOKEN}</code></ClickToCopy>
26
+ </div>
27
+ </li>
28
+ <li>
29
+ <div class="flex flex-col">
30
+ <div class="mb-2">Finally, install the extension using one of the following commands:</div>
31
+ <div>
32
+ <ClickToCopy @value={{concat "flb install fleetbase/" @options.extension.slug}}><code>flb install fleetbase/{{@options.extension.slug}}</code></ClickToCopy>
33
+ <div class="my-1 pl-2">or</div>
34
+ <ClickToCopy @value={{concat "flb install " @options.extension.public_id}}><code>flb install {{@options.extension.public_id}}</code></ClickToCopy>
35
+ </div>
36
+ </div>
37
+ </li>
38
+ </ol>
39
+ </div>
40
+ </Modal::Default>
@@ -1,7 +1,3 @@
1
1
  import Controller from '@ember/controller';
2
- import { tracked } from '@glimmer/tracking';
3
2
 
4
- export default class DevelopersExtensionsEditMonetizeController extends Controller {
5
- @tracked subscriptionModelOptions = ['flat_rate', 'tiered', 'usage'];
6
- @tracked billingPeriodOptions = ['daily', 'weekly', 'monthly', 'quarterly', 'yearly'];
7
- }
3
+ export default class DevelopersExtensionsEditMonetizeController extends Controller {}
@@ -1,7 +1,10 @@
1
1
  import Controller from '@ember/controller';
2
2
  import { tracked } from '@glimmer/tracking';
3
+ import { inject as service } from '@ember/service';
4
+ import { task } from 'ember-concurrency';
3
5
 
4
6
  export default class DevelopersPaymentsIndexController extends Controller {
7
+ @service fetch;
5
8
  @tracked hasStripeConnectAccount = true;
6
9
  @tracked table;
7
10
  @tracked page = 1;
@@ -37,4 +40,13 @@ export default class DevelopersPaymentsIndexController extends Controller {
37
40
  width: '20%',
38
41
  },
39
42
  ];
43
+
44
+ @task *lookupStripeConnectAccount() {
45
+ try {
46
+ const { hasStripeConnectAccount } = yield this.fetch.get('payments/has-stripe-connect-account', {}, { namespace: '~registry/v1' });
47
+ this.hasStripeConnectAccount = hasStripeConnectAccount;
48
+ } catch (error) {
49
+ this.hasStripeConnectAccount = false;
50
+ }
51
+ }
40
52
  }
@@ -63,8 +63,10 @@ export default class RegistryExtensionModel extends Model {
63
63
  @attr('array') languages;
64
64
  @attr('object') meta;
65
65
  @attr('boolean') core_service;
66
+ @attr('boolean') self_managed;
66
67
  @attr('boolean') is_purchased;
67
68
  @attr('boolean') is_installed;
69
+ @attr('boolean') is_author;
68
70
  @attr('string', { defaultValue: 'pending' }) status;
69
71
 
70
72
  /** @dates */
@@ -15,12 +15,7 @@ export default class DevelopersPaymentsIndexRoute extends Route {
15
15
  return this.fetch.get('payments/author-received', {}, { namespace: '~registry/v1' });
16
16
  }
17
17
 
18
- async setupController(controller) {
19
- try {
20
- const { hasStripeConnectAccount } = await this.fetch.get('payments/has-stripe-connect-account', {}, { namespace: '~registry/v1' });
21
- controller.hasStripeConnectAccount = hasStripeConnectAccount;
22
- } catch (error) {
23
- controller.hasStripeConnectAccount = false;
24
- }
18
+ setupController(controller) {
19
+ controller.lookupStripeConnectAccount.perform();
25
20
  }
26
21
  }
@@ -140,3 +140,34 @@ body[data-theme='dark'] .extension-card-container > .extension-card-body-contain
140
140
  margin-left: 2rem;
141
141
  margin-bottom: 2rem;
142
142
  }
143
+
144
+ .self-managed-install-instructions {
145
+ list-style: decimal;
146
+ color: #000;
147
+ padding-left: 30px;
148
+ font-size: 0.85rem;
149
+ line-height: 1rem;
150
+ }
151
+
152
+ .self-managed-install-instructions > li {
153
+ padding-bottom: 1.5rem;
154
+ }
155
+
156
+ body[data-theme='dark'] .self-managed-install-instructions {
157
+ list-style: decimal;
158
+ color: #fff;
159
+ padding-left: 30px;
160
+ }
161
+
162
+ .self-managed-install-instructions code {
163
+ display: flex;
164
+ font-family: monospace;
165
+ padding: 0.25rem 0.75rem;
166
+ box-shadow: 0 1px 2px 0 rgb(0 0 0 / 5%);
167
+ background-color: #1f2937;
168
+ border: 1px #374151 solid;
169
+ color: #86efac;
170
+ border-radius: 0.5rem;
171
+ font-size: 0.75rem;
172
+ line-height: 1rem;
173
+ }
@@ -1,34 +1,42 @@
1
1
  <Layout::Section::Header @title="Payments">
2
- <div class="flex flex-row space-x-1">
3
- <span class="text-sm text-black dark:text-white font-bold">Total Amount:</span>
4
- <span class="text-sm text-black dark:text-white">{{format-currency @model.total_amount "USD"}}</span>
5
- </div>
2
+ {{#if this.hasStripeConnectAccount}}
3
+ <div class="flex flex-row space-x-1">
4
+ <span class="text-sm text-black dark:text-white font-bold">Total Amount:</span>
5
+ <span class="text-sm text-black dark:text-white">{{format-currency @model.total_amount "USD"}}</span>
6
+ </div>
7
+ {{/if}}
6
8
  </Layout::Section::Header>
7
9
 
8
10
  <Layout::Section::Body class="overflow-y-scroll h-full">
9
- {{#if this.hasStripeConnectAccount}}
10
- <Table
11
- @rows={{@model.data}}
12
- @columns={{this.columns}}
13
- @selectable={{false}}
14
- @canSelectAll={{false}}
15
- @onSetup={{fn (mut this.table)}}
16
- @pagination={{true}}
17
- @paginationMeta={{@model.meta}}
18
- @page={{this.page}}
19
- @onPageChange={{fn (mut this.page)}}
20
- />
11
+ {{#if this.lookupStripeConnectAccount.isRunning}}
12
+ <div class="flex items-center justify-center flex-1 p-4">
13
+ <Spinner @loadingMessage="Loading monetization settings..." @loadingMessageClass="ml-2 text-black dark:text-white" @wrapperClass="flex flex-row items-center" />
14
+ </div>
21
15
  {{else}}
22
- <div class="container">
23
- <div class="max-w-3xl mx-auto mt-4">
24
- <div class="content">
25
- <div class="flex flex-col items-center justify-center">
26
- <h1 class="text-lg font-semibold mb-1">Your account is not setup to accept payments yet.</h1>
27
- <p class="text-sm mb-2">To accept payments for extensions, you must complete the onboard process via Stripe.</p>
28
- <Button @type="primary" @size="lg" @text="Start payments onboard" @onClick={{transition-to "developers.payments.onboard"}} />
16
+ {{#if this.hasStripeConnectAccount}}
17
+ <Table
18
+ @rows={{@model.data}}
19
+ @columns={{this.columns}}
20
+ @selectable={{false}}
21
+ @canSelectAll={{false}}
22
+ @onSetup={{fn (mut this.table)}}
23
+ @pagination={{true}}
24
+ @paginationMeta={{@model.meta}}
25
+ @page={{this.page}}
26
+ @onPageChange={{fn (mut this.page)}}
27
+ />
28
+ {{else}}
29
+ <div class="container">
30
+ <div class="max-w-3xl mx-auto mt-4">
31
+ <div class="content">
32
+ <div class="flex flex-col items-center justify-center">
33
+ <h1 class="text-lg font-semibold mb-1">Your account is not setup to accept payments yet.</h1>
34
+ <p class="text-sm mb-2">To accept payments for extensions, you must complete the onboard process via Stripe.</p>
35
+ <Button @type="primary" @size="lg" @text="Start payments onboard" @onClick={{transition-to "developers.payments.onboard"}} />
36
+ </div>
29
37
  </div>
30
38
  </div>
31
39
  </div>
32
- </div>
40
+ {{/if}}
33
41
  {{/if}}
34
42
  </Layout::Section::Body>
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/registry-bridge-engine/components/modals/self-managed-install-instructions';
package/composer.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/registry-bridge",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Internal Bridge between Fleetbase API and Extensions Registry",
5
5
  "keywords": [
6
6
  "fleetbase-extension",
package/extension.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Registry Bridge",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Internal Bridge between Fleetbase API and Extensions Registry",
5
5
  "repository": "https://github.com/fleetbase/registry-bridge",
6
6
  "license": "AGPL-3.0-or-later",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/registry-bridge-engine",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Internal Bridge between Fleetbase API and Extensions Registry",
5
5
  "fleetbase": {
6
6
  "route": "extensions"
@@ -40,7 +40,7 @@
40
40
  "dependencies": {
41
41
  "@babel/core": "^7.23.2",
42
42
  "@fleetbase/ember-core": "^0.2.14",
43
- "@fleetbase/ember-ui": "^0.2.20",
43
+ "@fleetbase/ember-ui": "^0.2.21",
44
44
  "@fortawesome/ember-fontawesome": "^2.0.0",
45
45
  "@fortawesome/fontawesome-svg-core": "6.4.0",
46
46
  "@fortawesome/free-solid-svg-icons": "6.4.0",
@@ -0,0 +1,28 @@
1
+ <?php
2
+
3
+ use Illuminate\Database\Migrations\Migration;
4
+ use Illuminate\Database\Schema\Blueprint;
5
+ use Illuminate\Support\Facades\Schema;
6
+
7
+ return new class extends Migration
8
+ {
9
+ /**
10
+ * Run the migrations.
11
+ */
12
+ public function up(): void
13
+ {
14
+ Schema::table('registry_extensions', function (Blueprint $table) {
15
+ $table->boolean('self_managed')->after('subtitle')->default(0);
16
+ });
17
+ }
18
+
19
+ /**
20
+ * Reverse the migrations.
21
+ */
22
+ public function down(): void
23
+ {
24
+ Schema::table('registry_extensions', function (Blueprint $table) {
25
+ $table->dropColumn('self_managed');
26
+ });
27
+ }
28
+ };
@@ -40,21 +40,27 @@ class RegistryAuthController extends Controller
40
40
  return response()->error('Invalid registry token provided for authentication.', 401);
41
41
  }
42
42
 
43
- // Fetch unauthorized extensions
44
- $unauthorizedExtensions = RegistryExtension::where('payment_required', true)
45
- ->whereDoesntHave('purchases', function ($query) use ($registryUser) {
46
- $query->where('company_uuid', $registryUser->company_uuid);
47
- })
48
- ->whereHas('currentBundle')
49
- ->with('currentBundle')
50
- ->get();
51
-
52
- // Map to unathorized to package names
53
- $unauthorizedExtensionNames = $unauthorizedExtensions->map(function ($registryExtension) {
54
- $composerJson = $registryExtension->currentBundle->meta['composer.json'] ?? [];
55
-
56
- return $composerJson['name'] ?? null;
57
- })->filter()->values();
43
+ // Init unauthorized extensions
44
+ $unauthorizedExtensionNames = collect();
45
+
46
+ // Unless admin the registry user is only allowed access to their extensions
47
+ if ($registryUser->isNotAdmin()) {
48
+ // Fetch unauthorized extensions
49
+ $unauthorizedExtensions = RegistryExtension::where('payment_required', true)
50
+ ->whereDoesntHave('purchases', function ($query) use ($registryUser) {
51
+ $query->where('company_uuid', $registryUser->company_uuid);
52
+ })
53
+ ->whereHas('currentBundle')
54
+ ->with('currentBundle')
55
+ ->get();
56
+
57
+ // Map to unathorized to package names
58
+ $unauthorizedExtensionNames = $unauthorizedExtensions->map(function ($registryExtension) {
59
+ $composerJson = $registryExtension->currentBundle->meta['composer.json'] ?? [];
60
+
61
+ return $composerJson['name'] ?? null;
62
+ })->filter()->values();
63
+ }
58
64
 
59
65
  // Done
60
66
  return response()->json([
@@ -6,6 +6,7 @@ use Fleetbase\Http\Controllers\Controller;
6
6
  use Fleetbase\Http\Resources\Category as CategoryResource;
7
7
  use Fleetbase\Models\Category;
8
8
  use Fleetbase\RegistryBridge\Models\RegistryExtension;
9
+ use Illuminate\Http\Request;
9
10
 
10
11
  class RegistryController extends Controller
11
12
  {
@@ -51,4 +52,49 @@ class RegistryController extends Controller
51
52
 
52
53
  return response()->json($installedExtensions);
53
54
  }
55
+
56
+ /**
57
+ * Lookup and retrieve package information based on the provided package name.
58
+ *
59
+ * This method handles a request to lookup a package by its name. It utilizes the `RegistryExtension::lookup` method to find the
60
+ * corresponding registry extension. If no extension is found or if the extension does not have valid package or composer data,
61
+ * an error response is returned.
62
+ *
63
+ * If a valid extension and its associated bundle are found, the function extracts the package and composer names from the
64
+ * `package.json` and `composer.json` metadata. These names are then returned in a JSON response.
65
+ *
66
+ * @param Request $request the incoming HTTP request containing the 'package' input parameter
67
+ *
68
+ * @return \Illuminate\Http\JsonResponse a JSON response containing the package and composer names if found, or an error message otherwise
69
+ */
70
+ public function lookupPackage(Request $request)
71
+ {
72
+ $packageName = $request->input('package');
73
+ $registryExtension = RegistryExtension::lookup($packageName);
74
+ if (!$registryExtension) {
75
+ return response()->error('No extension found by this name for install');
76
+ }
77
+
78
+ if (!$registryExtension->currentBundle) {
79
+ return response()->error('No valid package data found for this extension install');
80
+ }
81
+
82
+ $packageJson = $registryExtension->currentBundle->meta['package.json'];
83
+ if (!$packageJson) {
84
+ return response()->error('No valid package data found for this extension install');
85
+ }
86
+
87
+ $composerJson = $registryExtension->currentBundle->meta['composer.json'];
88
+ if (!$composerJson) {
89
+ return response()->error('No valid package data found for this extension install');
90
+ }
91
+
92
+ $packageJsonName = data_get($packageJson, 'name');
93
+ $composerJsonName = data_get($composerJson, 'name');
94
+
95
+ return response()->json([
96
+ 'npm' => $packageJsonName,
97
+ 'composer' => $composerJsonName,
98
+ ]);
99
+ }
54
100
  }
@@ -15,6 +15,7 @@ use Fleetbase\Traits\HasMetaAttributes;
15
15
  use Fleetbase\Traits\HasPublicId;
16
16
  use Fleetbase\Traits\HasUuid;
17
17
  use Fleetbase\Traits\Searchable;
18
+ use Illuminate\Support\Str;
18
19
  use Spatie\Sluggable\HasSlug;
19
20
  use Spatie\Sluggable\SlugOptions;
20
21
 
@@ -57,6 +58,7 @@ class RegistryExtension extends Model
57
58
  'stripe_product_id',
58
59
  'name',
59
60
  'subtitle',
61
+ 'self_managed',
60
62
  'payment_required',
61
63
  'price',
62
64
  'sale_price',
@@ -90,6 +92,7 @@ class RegistryExtension extends Model
90
92
  * The attributes that should be cast to native types.
91
93
  */
92
94
  protected $casts = [
95
+ 'self_managed' => 'boolean',
93
96
  'payment_required' => 'boolean',
94
97
  'on_sale' => 'boolean',
95
98
  'subscription_required' => 'boolean',
@@ -121,6 +124,7 @@ class RegistryExtension extends Model
121
124
  'publisher_name',
122
125
  'is_purchased',
123
126
  'is_installed',
127
+ 'is_author',
124
128
  ];
125
129
 
126
130
  /**
@@ -413,6 +417,14 @@ class RegistryExtension extends Model
413
417
  return data_get($this, 'company.name');
414
418
  }
415
419
 
420
+ /**
421
+ * Determines if the current company session is the author of the extension.
422
+ */
423
+ public function getIsAuthorAttribute(): bool
424
+ {
425
+ return $this->company_uuid === session('company');
426
+ }
427
+
416
428
  /**
417
429
  * Finds a RegistryExtension by package name in the associated currentBundle.
418
430
  *
@@ -433,6 +445,39 @@ class RegistryExtension extends Model
433
445
  })->first();
434
446
  }
435
447
 
448
+ /**
449
+ * Lookup a registry extension based on the given package name.
450
+ *
451
+ * This method attempts to find a `RegistryExtension` that matches the provided package name. It checks multiple fields including
452
+ * `uuid`, `public_id`, and `slug`. If the package name starts with 'fleetbase/', it also attempts to match the slug extracted from the package name.
453
+ *
454
+ * Additionally, the method checks for the existence of a related `currentBundle` where the `package.json` or `composer.json` metadata
455
+ * matches the provided package name.
456
+ *
457
+ * @param string $packageName the name, UUID, public ID, or slug of the package to lookup
458
+ *
459
+ * @return RegistryExtension|null returns the found `RegistryExtension` instance or `null` if no match is found
460
+ */
461
+ public static function lookup(string $packageName): ?RegistryExtension
462
+ {
463
+ return static::where('status', 'published')->where(function ($query) use ($packageName) {
464
+ $query->where('uuid', $packageName)
465
+ ->orWhere('public_id', $packageName)
466
+ ->orWhere('slug', $packageName);
467
+
468
+ // Check for fleetbase/ prefix and match slug
469
+ if (Str::startsWith($packageName, 'fleetbase/')) {
470
+ $packageSlug = explode('/', $packageName)[1] ?? null;
471
+ if ($packageSlug) {
472
+ $query->orWhere('slug', $packageSlug);
473
+ }
474
+ }
475
+ })->orWhereHas('currentBundle', function ($query) use ($packageName) {
476
+ $query->where('meta->package.json->name', $packageName)
477
+ ->orWhere('meta->composer.json->name', $packageName);
478
+ })->with(['currentBundle'])->first();
479
+ }
480
+
436
481
  /**
437
482
  * Determines if the current extension instance is ready for submission.
438
483
  *
@@ -688,7 +688,7 @@ class RegistryExtensionBundle extends Model
688
688
  ]);
689
689
 
690
690
  // minimal latency
691
- usleep(500 * rand(2, 4));
691
+ usleep(50 * rand(1, 3));
692
692
  }
693
693
  }
694
694
  }
@@ -708,7 +708,7 @@ class RegistryExtensionBundle extends Model
708
708
  ]);
709
709
 
710
710
  // minimal latency
711
- usleep(500 * rand(2, 4));
711
+ usleep(50 * rand(1, 3));
712
712
  }
713
713
  }
714
714
  }
@@ -12,7 +12,8 @@ use Illuminate\Support\Facades\Route;
12
12
  | is assigned the "api" middleware group. Enjoy building your API!
13
13
  |
14
14
  */
15
-
15
+ // Lookup package endpoint
16
+ Route::get(config('internals.api.routing.prefix', '~registry') . '/v1/lookup', 'Fleetbase\RegistryBridge\Http\Controllers\Internal\v1\RegistryController@lookupPackage');
16
17
  Route::prefix(config('internals.api.routing.prefix', '~registry'))->middleware(['fleetbase.registry'])->namespace('Fleetbase\RegistryBridge\Http\Controllers')->group(
17
18
  function ($router) {
18
19
  /*
@@ -9,6 +9,16 @@ registry-bridge:
9
9
  about: About
10
10
  about-extension: About {extensionName}
11
11
  component:
12
+ extension-details-modal:
13
+ extension: Extension
14
+ author: Author
15
+ overview: Overview
16
+ details: Details
17
+ version: Version
18
+ updated: Updated
19
+ website: Website
20
+ self-managed: Self Managed
21
+ self-managed-help-text: A self-managed extension is designed for users who host Fleetbase on their own servers, outside of the cloud/SaaS environment. These extensions require manual installation and configuration. If you are using Fleetbase as a self-hosted instance, you can use these extensions to add additional features. However, they are not available for cloud/SaaS users.
12
22
  extension-pending-publish-viewer:
13
23
  content-panel-title: Extensions Pending Publish
14
24
  focused-extension-title: >
@@ -116,4 +126,6 @@ registry-bridge:
116
126
  extension-id: Extension ID
117
127
  extension-id-help-text: The unique identifier for this extension.
118
128
  bundles: Bundles
119
- upload-new-bundle: Upload new Bundle
129
+ upload-new-bundle: Upload new Bundle
130
+ self-managed: Self Managed
131
+ self-managed-help-text: Enable this option if your module is intended for self-hosted instances only. By selecting this, the module will not be available for installation on the cloud/SaaS version of Fleetbase and must be manually installed by the user on their own server.