Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions common/config/rush/command-line.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,14 @@
"safeForSimultaneousRushProcesses": true,
"shellCommand": "cd ./dev && docker compose -f docker-compose.yaml -f docker-compose.min.yaml up -d --force-recreate"
},
{
"commandKind": "global",
"name": "docker:up:ext",
"summary": "Up extended development stack with billing + payment",
"description": "Start extended docker compose (docker-compose.yaml + docker-compose.ext.yaml) with the optional billing and payment services enabled. Required env: STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_SUBSCRIPTION_PLANS (or POLAR_* equivalents).",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "cd ./dev && docker compose -f docker-compose.yaml -f docker-compose.ext.yaml up -d --force-recreate"
},
{
"commandKind": "global",
"name": "docker:up:pg",
Expand Down
8 changes: 7 additions & 1 deletion common/config/rush/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 80 additions & 0 deletions dev/docker-compose.ext.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Ext overlay: enables the optional billing + payment services so that the client
# can reach them and plan-limit restrictions kick in.
# Use with: docker compose -f docker-compose.yaml -f docker-compose.ext.yaml up
# or: rush docker:up:ext
# (or: docker compose -f docker-compose.yaml -f docker-compose.min.yaml -f docker-compose.ext.yaml up
# if you want to keep the small footprint and just add billing/payment on top.)
#
# The base docker-compose.yaml already advertises PAYMENT_URL=http://huly.local:3040
# to the front. This overlay materialises the matching `payment` and `billing`
# services and adds BILLING_URL to the front so the client picks them up.
#
# Provider credentials are read from the host environment (e.g. dev/.env or shell):
# PAYMENT_USE_SANDBOX default: true
# POLAR_ACCESS_TOKEN, POLAR_WEBHOOK_SECRET, POLAR_ORGANIZATION_ID,
# POLAR_SUBSCRIPTION_PLANS
# STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_SUBSCRIPTION_PLANS
# All are optional — without them the service starts in sandbox mode.

services:
payment:
image: hardcoreeng/payment
# Clear the profile gate from docker-compose.min.yaml so the service starts
# whenever this overlay is included, even alongside `min`.
profiles: !override []
extra_hosts:
- 'huly.local:host-gateway'
depends_on:
account:
condition: service_started
ports:
- 3040:3040
environment:
- PORT=3040
- SECRET=secret
- ACCOUNTS_URL=http://huly.local:3000
- FRONT_URL=http://huly.local:8087
- USE_SANDBOX=${PAYMENT_USE_SANDBOX:-true}
# Provider credentials — supply via .env / shell env. Empty values keep
# the service running in safe "sandbox" mode without making outbound calls.
# - POLAR_ACCESS_TOKEN=${POLAR_ACCESS_TOKEN:-}
# - POLAR_WEBHOOK_SECRET=${POLAR_WEBHOOK_SECRET:-}
# - POLAR_ORGANIZATION_ID=${POLAR_ORGANIZATION_ID:-}
# - POLAR_SUBSCRIPTION_PLANS=${POLAR_SUBSCRIPTION_PLANS:-}
- STRIPE_API_KEY=${STRIPE_API_KEY:-}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET:-}
- STRIPE_SUBSCRIPTION_PLANS=${STRIPE_SUBSCRIPTION_PLANS:-}
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
restart: unless-stopped

billing:
image: hardcoreeng/billing
profiles: !override []
extra_hosts:
- 'huly.local:host-gateway'
depends_on:
cockroach:
condition: service_started
minio:
condition: service_healthy
account:
condition: service_started
ports:
- 4042:4042
environment:
- PORT=4042
- SECRET=secret
- ACCOUNTS_URL=http://huly.local:3000
- DB_URL=${DB_CR_URL}
- STORAGE_CONFIG=${STORAGE_CONFIG}
# Recheck workspace usage every 10 minutes in dev (default is 1 hour).
- USAGE_UPDATE_INTERVAL=600
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
restart: unless-stopped

# Make the billing endpoint discoverable by the client. PAYMENT_URL is already
# set in the base compose file so we don't repeat it here — environment values
# are merged additively.
front:
environment:
- BILLING_URL=http://huly.local:4042
4 changes: 3 additions & 1 deletion dev/docker-compose.min.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Min override: excludes optional services from min deployment.
# Use with: docker compose -f docker-compose.yaml -f docker-compose.min.yaml up
# (rush docker:up:min)
# Excluded: preview, link-preview, elastic, redis, stats, payment, fulltext_cockroach,
# Excluded: preview, link-preview, elastic, redis, stats, payment, billing, fulltext_cockroach,
# print, sign, hulykvs, hulygun, hulypulse, process-service, backup-cockroach,
# backup-api, rating_cockroach
# Overrides below remove dependencies on excluded services so the min project validates.
Expand All @@ -19,6 +19,8 @@ services:
profiles: ["full"]
payment:
profiles: ["full"]
billing:
profiles: ["full"]
fulltext_cockroach:
profiles: ["full"]
print:
Expand Down
1 change: 1 addition & 0 deletions foundations/core/packages/core/src/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,7 @@ export interface UsageStatus {
usage: Record<string, number>
startTime: Timestamp
updateTime: Timestamp
limitsExceededSince?: Timestamp // Timestamp when current usage first exceeded the workspace plan limits.
}

export interface WorkspaceInfoWithStatus extends WorkspaceInfo {
Expand Down
18 changes: 9 additions & 9 deletions models/billing/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Model, UX, type Builder } from '@hcengineering/model'
import core, { TDoc } from '@hcengineering/model-core'
import { type IntlString } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import billing, { type Tier } from '@hcengineering/billing'
import billing, { TIER_LIMITS_GB, type Tier } from '@hcengineering/billing'
import { AccountRole, DOMAIN_MODEL } from '@hcengineering/core'
import presentation from '@hcengineering/model-presentation'
import workbench from '@hcengineering/workbench'
Expand Down Expand Up @@ -58,8 +58,8 @@ export function createModel (builder: Builder): void {
{
label: billing.string.Common,
description: billing.string.CommonDescription,
storageLimitGB: 10,
trafficLimitGB: 10,
storageLimitGB: TIER_LIMITS_GB.common.storageGB,
trafficLimitGB: TIER_LIMITS_GB.common.trafficGB,
priceMonthly: 0,
index: 0
},
Expand All @@ -72,8 +72,8 @@ export function createModel (builder: Builder): void {
{
label: billing.string.Rare,
description: billing.string.RareDescription,
storageLimitGB: 100,
trafficLimitGB: 100,
storageLimitGB: TIER_LIMITS_GB.rare.storageGB,
trafficLimitGB: TIER_LIMITS_GB.rare.trafficGB,
priceMonthly: 19.99,
index: 1,
color: 'Sky'
Expand All @@ -87,8 +87,8 @@ export function createModel (builder: Builder): void {
{
label: billing.string.Epic,
description: billing.string.EpicDescription,
storageLimitGB: 1000,
trafficLimitGB: 500,
storageLimitGB: TIER_LIMITS_GB.epic.storageGB,
trafficLimitGB: TIER_LIMITS_GB.epic.trafficGB,
priceMonthly: 99.99,
index: 2,
color: 'Orchid'
Expand All @@ -102,8 +102,8 @@ export function createModel (builder: Builder): void {
{
label: billing.string.Legendary,
description: billing.string.LegendaryDescription,
storageLimitGB: 10000,
trafficLimitGB: 2000,
storageLimitGB: TIER_LIMITS_GB.legendary.storageGB,
trafficLimitGB: TIER_LIMITS_GB.legendary.trafficGB,
priceMonthly: 399.99,
index: 3,
color: 'Orange'
Expand Down
42 changes: 42 additions & 0 deletions packages/presentation/src/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,53 @@ export function getFileUrl (file: string, filename?: string): string {
return storage.getFileUrl(workspace, file, filename)
}

/**
* Error thrown by registered upload guards (see {@link setUploadGuard}) when the
* current workspace is not allowed to upload new files (e.g. plan limit reached
* and grace period expired). Caller code should handle this distinct from generic
* upload failures and surface a user-friendly message + upgrade CTA.
*
* @public
*/
export class UploadRestrictedError extends Error {
constructor (
public readonly reason: string,
message?: string
) {
super(message ?? reason)
this.name = 'UploadRestrictedError'
}
}

/** @public */
export type UploadGuard = (file: File) => Promise<void> | void

let uploadGuard: UploadGuard | undefined

/**
* Register a synchronous/async guard called before every {@link uploadFile}.
* Throw an {@link UploadRestrictedError} from the guard to block the upload.
* Pass `undefined` to clear the guard.
*
* The guard lives in `presentation` to keep upload restriction concerns out of
* every individual call site, and to avoid a dependency from `presentation` to
* higher-level plugins (billing-resources) — DI inversion via a setter.
*
* @public
*/
export function setUploadGuard (guard: UploadGuard | undefined): void {
uploadGuard = guard
}

/** @public */
export async function uploadFile (
file: File,
uuid?: Ref<PlatformBlob>
): Promise<{ uuid: Ref<PlatformBlob>, metadata: Record<string, any> }> {
if (uploadGuard !== undefined) {
await uploadGuard(file)
}

uuid ??= generateFileId() as Ref<PlatformBlob>

const token = getToken()
Expand Down
19 changes: 19 additions & 0 deletions packages/theme/styles/_colors.scss
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@
--theme-button-attention-active-shadow: 0 4px 12px rgba(124, 58, 237, 0.6);
--theme-button-attention-focus-shadow: 0 0 0 3px rgba(124, 58, 237, 0.3);

--theme-button-warning-color: #FB923C;
--theme-button-warning-bg: rgba(249, 115, 22, 0.12);
--theme-button-warning-border: rgba(249, 115, 22, 0.32);
--theme-button-warning-hover-bg: rgba(249, 115, 22, 0.18);
--theme-button-warning-hover-border: rgba(249, 115, 22, 0.45);
--theme-button-warning-active-bg: rgba(249, 115, 22, 0.24);
--theme-button-warning-focus-ring: 0 0 0 2px rgba(249, 115, 22, 0.35);

--theme-refinput-divider: rgba(255, 255, 255, .07);
--theme-refinput-border: rgba(255, 255, 255, .1);

Expand Down Expand Up @@ -467,6 +475,17 @@
--theme-button-attention-active-shadow: 0 4px 12px rgba(236, 72, 153, 0.5);
--theme-button-attention-focus-shadow: 0 0 0 3px rgba(236, 72, 153, 0.2);

// Warning button — subdued, tinted-orange variant for persistent slots
// (e.g. sidebar footer). Designed to read as "heads up" without competing
// for attention with primary actions. No gradients, no lift, no pulse.
--theme-button-warning-color: #C2410C;
--theme-button-warning-bg: rgba(249, 115, 22, 0.10);
--theme-button-warning-border: rgba(249, 115, 22, 0.30);
--theme-button-warning-hover-bg: rgba(249, 115, 22, 0.16);
--theme-button-warning-hover-border: rgba(249, 115, 22, 0.45);
--theme-button-warning-active-bg: rgba(249, 115, 22, 0.22);
--theme-button-warning-focus-ring: 0 0 0 2px rgba(249, 115, 22, 0.3);

--theme-refinput-divider: rgba(0, 0, 0, .07);
--theme-refinput-border: rgba(0, 0, 0, .1);

Expand Down
70 changes: 69 additions & 1 deletion packages/theme/styles/button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@
border-color: var(--theme-button-attention-hover-border);
transform: translateY(-2px);
box-shadow: var(--theme-button-attention-hover-shadow);

&::before { left: 100%; }
}

Expand Down Expand Up @@ -228,6 +228,41 @@
}
}

&.warning {
font-weight: 500;
border: 1px solid var(--theme-button-warning-border);
background: var(--theme-button-warning-bg);

.icon { color: var(--theme-button-warning-color); }
span { color: var(--theme-button-warning-color); }

&:not(.disabled, :disabled):hover {
background: var(--theme-button-warning-hover-bg);
border-color: var(--theme-button-warning-hover-border);
}

&:not(.disabled, :disabled):active,
&.pressed:not(.disabled, :disabled) {
background: var(--theme-button-warning-active-bg);
}

&:not(.no-focus):focus {
box-shadow: var(--theme-button-warning-focus-ring);
}

&:disabled:not(.loading),
&.disabled:not(.loading) {
background: var(--button-disabled-BackgroundColor);
border-color: var(--button-disabled-BackgroundColor);
}

&.loading {
background: var(--theme-button-warning-active-bg);

span { color: var(--theme-button-warning-color); }
}
}

& > * { pointer-events: none; }
}

Expand Down Expand Up @@ -648,6 +683,39 @@
.btn-right-icon { color: var(--primary-button-disabled-color); }
}
}
&.warning {
font-weight: 500;
color: var(--theme-button-warning-color);
background: var(--theme-button-warning-bg);
border: 1px solid var(--theme-button-warning-border);

.btn-icon,
.btn-right-icon { color: var(--theme-button-warning-color); }

&:hover {
background: var(--theme-button-warning-hover-bg);
border-color: var(--theme-button-warning-hover-border);
}

&:active,
&.pressed,
&.pressed:hover {
background: var(--theme-button-warning-active-bg);
}

&:not(.no-focus):focus {
box-shadow: var(--theme-button-warning-focus-ring);
}

&:disabled {
color: var(--primary-button-disabled-color);
background: var(--primary-button-disabled);
border-color: var(--primary-button-disabled);

.btn-icon,
.btn-right-icon { color: var(--primary-button-disabled-color); }
}
}
&.contrast {
padding: .75rem 1rem;
font-weight: 500;
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export type ButtonKind =
| 'contrast'
| 'stepper'
| 'attention'
| 'warning'
export type ButtonSize = 'inline' | 'x-small' | 'small' | 'medium' | 'large' | 'x-large'
export type ButtonShape =
| 'rectangle'
Expand Down
Loading
Loading