@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.
- package/addon/components/extension-card.hbs +5 -0
- package/addon/components/extension-card.js +46 -1
- package/addon/components/extension-form.hbs +20 -2
- package/addon/components/extension-form.js +57 -0
- package/addon/components/extension-monetize-form.hbs +63 -43
- package/addon/components/extension-monetize-form.js +18 -0
- package/addon/components/modals/extension-details.hbs +34 -15
- package/addon/components/modals/self-managed-install-instructions.hbs +40 -0
- package/addon/controllers/developers/extensions/edit/monetize.js +1 -5
- package/addon/controllers/developers/payments/index.js +12 -0
- package/addon/models/registry-extension.js +2 -0
- package/addon/routes/developers/payments/index.js +2 -7
- package/addon/styles/registry-bridge-engine.css +31 -0
- package/addon/templates/developers/payments/index.hbs +32 -24
- package/app/components/modals/self-managed-install-instructions.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +2 -2
- package/server/migrations/2024_08_02_072214_add_self_managed_column_to_registry_extensions_table.php +28 -0
- package/server/src/Http/Controllers/Internal/v1/RegistryAuthController.php +21 -15
- package/server/src/Http/Controllers/Internal/v1/RegistryController.php +46 -0
- package/server/src/Models/RegistryExtension.php +45 -0
- package/server/src/Models/RegistryExtensionBundle.php +2 -2
- package/server/src/routes.php +2 -1
- package/translations/en-us.yaml +13 -1
|
@@ -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
|
|
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
|
-
|
|
3
|
-
<
|
|
4
|
-
@
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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="
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
10
|
-
<
|
|
11
|
-
@
|
|
12
|
-
|
|
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
|
-
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
package/extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fleetbase/registry-bridge-engine",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
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",
|
package/server/migrations/2024_08_02_072214_add_self_managed_column_to_registry_extensions_table.php
ADDED
|
@@ -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
|
-
//
|
|
44
|
-
$
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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(
|
|
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(
|
|
711
|
+
usleep(50 * rand(1, 3));
|
|
712
712
|
}
|
|
713
713
|
}
|
|
714
714
|
}
|
package/server/src/routes.php
CHANGED
|
@@ -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
|
/*
|
package/translations/en-us.yaml
CHANGED
|
@@ -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.
|