964 lines
44 KiB
Dart
964 lines
44 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
|
|
/// API Configuration for BottleCRM
|
|
///
|
|
/// Provides centralized configuration for API endpoints. Point a build at any
|
|
/// server with `--dart-define=API_BASE_URL=...` rather than editing this file.
|
|
class ApiConfig {
|
|
/// A debug build with no override talks to a backend on this machine, never
|
|
/// a remote host: it would receive every sign-in token.
|
|
static const String _developmentUrl = 'http://localhost:8000';
|
|
|
|
/// Production API URL
|
|
static const String _productionUrl = 'https://api.bottlecrm.io';
|
|
|
|
/// Public marketing site. Hosts the documents the app has to link to
|
|
/// (`/terms-of-service`, `/privacy-policy`) rather than ship its own copies,
|
|
/// which would drift the moment either is edited.
|
|
static const String marketingSite = 'https://bottlecrm.io';
|
|
|
|
/// Build-time override, so somebody self-hosting points a build at their own
|
|
/// server without editing this file:
|
|
///
|
|
/// flutter build apk --release --dart-define=API_BASE_URL=https://crm.example.com
|
|
///
|
|
/// Compile-time on purpose. A runtime setting would mean anything able to
|
|
/// write the app's storage could redirect every request, and every JWT with
|
|
/// it, to a server of its own choosing.
|
|
static const String _baseUrlOverride = String.fromEnvironment('API_BASE_URL');
|
|
|
|
/// Google's Web OAuth client, the audience the backend's `GOOGLE_CLIENT_ID`
|
|
/// verifies against. Overridable the same way, because a self-hoster signs in
|
|
/// against their own Google project:
|
|
///
|
|
/// --dart-define=GOOGLE_SERVER_CLIENT_ID=<id>.apps.googleusercontent.com
|
|
///
|
|
/// Not a secret. A client id is public by design; the backend is what decides
|
|
/// whether an ID token is acceptable.
|
|
static const String googleServerClientId = String.fromEnvironment(
|
|
'GOOGLE_SERVER_CLIENT_ID',
|
|
defaultValue:
|
|
'1072513761792-p59rct7b1c3go7l58e51r3geuqff2tfl.apps.googleusercontent.com',
|
|
);
|
|
|
|
/// The API host this build talks to.
|
|
static final String baseUrl = resolveBaseUrl(
|
|
override: _baseUrlOverride,
|
|
isDebug: kDebugMode,
|
|
);
|
|
|
|
/// The override rules, separated from the build constants so both branches
|
|
/// can be tested. `flutter test` runs one build, so a `--dart-define` the
|
|
/// suite does not pass is a branch no test could otherwise reach.
|
|
@visibleForTesting
|
|
static String resolveBaseUrl({
|
|
required String override,
|
|
required bool isDebug,
|
|
}) {
|
|
if (override.isEmpty) {
|
|
return isDebug ? _developmentUrl : _productionUrl;
|
|
}
|
|
// A trailing slash would produce `https://host//api`, and the resulting 404
|
|
// on every request says nothing about the cause.
|
|
final trimmed = override.endsWith('/')
|
|
? override.substring(0, override.length - 1)
|
|
: override;
|
|
// A release build talking plain HTTP puts every JWT on the wire in the
|
|
// clear. Refusing loudly beats falling back to the default, which would
|
|
// leave a self-hoster looking at somebody else's server and no explanation.
|
|
// `http://` stays allowed in debug: that is how anyone runs against a local
|
|
// Django.
|
|
if (!isDebug && !trimmed.startsWith('https://')) {
|
|
throw StateError(
|
|
'API_BASE_URL must start with https:// in a release build. '
|
|
'Got "$trimmed", which would send every token unencrypted.',
|
|
);
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
/// API base path
|
|
static String get apiBaseUrl => '$baseUrl/api';
|
|
|
|
// ==========================================================================
|
|
// AUTHENTICATION ENDPOINTS
|
|
// ==========================================================================
|
|
|
|
/// Google Sign-In with ID token (same as old app)
|
|
static String get googleLogin => '$apiBaseUrl/auth/google/';
|
|
|
|
/// Request a passwordless sign-in code (OTP) by email
|
|
static String get magicLinkRequest => '$apiBaseUrl/auth/magic-link/request/';
|
|
|
|
/// Verify a 6-digit OTP code and exchange it for JWT tokens
|
|
static String get magicLinkVerifyCode =>
|
|
'$apiBaseUrl/auth/magic-link/verify-code/';
|
|
|
|
/// Refresh JWT token
|
|
static String get refreshToken => '$apiBaseUrl/auth/refresh-token/';
|
|
|
|
/// Sign out: blacklists the refresh token server-side. Needs no access
|
|
/// token, which is what lets it work when the session has already expired.
|
|
static String get logout => '$apiBaseUrl/auth/logout/';
|
|
|
|
/// Get current user info
|
|
static String get me => '$apiBaseUrl/auth/me/';
|
|
|
|
/// Get user profile with org details (read; ProfileDetailView)
|
|
static String get profile => '$apiBaseUrl/auth/profile/';
|
|
|
|
/// Update user profile, only `phone` is editable per backend contract.
|
|
/// Distinct endpoint from `profile` above (ProfileView vs ProfileDetailView).
|
|
static String get profileUpdate => '$apiBaseUrl/profile/';
|
|
|
|
/// Switch organization context
|
|
static String get switchOrg => '$apiBaseUrl/auth/switch-org/';
|
|
|
|
/// Create an organization and become its admin (OrgProfileCreateView).
|
|
/// Needs a signed-in user but no org context, which is the point: the caller
|
|
/// is someone who has none yet.
|
|
static String get orgCreate => '$apiBaseUrl/org/';
|
|
|
|
/// IANA zone names with their current UTC offsets, for the org form's
|
|
/// timezone picker. Served rather than read from the device so both clients
|
|
/// use one vocabulary (see `common/org_time.py`).
|
|
static String get timezones => '$apiBaseUrl/org/timezones/';
|
|
|
|
/// The org's own settings. GET is open to any member (the company profile is
|
|
/// not a secret), PATCH is admin-only. Always acts on the caller's own org:
|
|
/// there is no id in the path, so there is nothing to scope.
|
|
///
|
|
/// The org API key is not on this payload in any form. It authenticates as
|
|
/// the org's first admin and is rotated through `OrgApiKeyView` alone.
|
|
static String get orgSettings => '$apiBaseUrl/org/settings/';
|
|
|
|
/// The public help center switch and address. GET is open to any member and
|
|
/// carries `can_edit`; PATCH is admin-only. Always the caller's own org.
|
|
static String get helpCenterSettings => '$apiBaseUrl/org/help-center/';
|
|
|
|
/// Vertical packs available to apply. Needs authentication but no org
|
|
/// context, because its first caller is someone creating their first org.
|
|
static String get packs => '$apiBaseUrl/packs/';
|
|
|
|
/// Apply one pack. Admin-only, additive-only, and safe to repeat: a pack
|
|
/// already applied reports everything skipped rather than refusing.
|
|
static String packApply(String packId) => '$apiBaseUrl/packs/$packId/apply/';
|
|
|
|
/// Delete the demo records a pack created for this org, and nothing else.
|
|
/// Admin-only. A sample record the org has since attached real work to is
|
|
/// deliberately kept and reported in `retained_by_type`.
|
|
static String get packSampleData => '$apiBaseUrl/packs/sample-data/';
|
|
|
|
// ==========================================================================
|
|
// API TOKENS
|
|
// ==========================================================================
|
|
|
|
/// **The admin's org-wide oversight list.** Every token in the org, so a
|
|
/// deactivated owner's row and a forgotten one can be found. ADMIN-only: a
|
|
/// member gets 403, which is why the screen gates on role.
|
|
///
|
|
/// Only `token_prefix` is ever returned. The server keeps a SHA-256 hash and
|
|
/// no raw value exists to fetch.
|
|
static String get orgTokens => '$apiBaseUrl/org/tokens/';
|
|
|
|
/// Revoke any token in the org. Admin-only, org-scoped, idempotent. Separate
|
|
/// from the self-scoped path below so the "a user manages only their own"
|
|
/// guard there is never widened.
|
|
static String orgToken(String id) => '$apiBaseUrl/org/tokens/$id/';
|
|
|
|
/// Create a token **for yourself**. The server sets the owner from the
|
|
/// caller's own profile, so there is no way to mint one for someone else.
|
|
/// The raw value comes back once, in this response, and never again.
|
|
static String get profileTokens => '$apiBaseUrl/profile/tokens/';
|
|
|
|
/// Revoke one of **your own**. Self-scoped server-side, so a colleague's id
|
|
/// 404s rather than being revoked. Distinct from `orgToken(id)`, which is
|
|
/// the admin's org-wide revoke and refuses a member outright.
|
|
static String profileToken(String id) => '$apiBaseUrl/profile/tokens/$id/';
|
|
|
|
/// Your own task calendar feed: `GET` the state, `POST` to turn it on or
|
|
/// replace its URL (returned once), `DELETE` to turn it off. Self-scoped and
|
|
/// session-only server-side: no API token can reach it.
|
|
static String get calendarFeed => '$apiBaseUrl/profile/calendar-feed/';
|
|
|
|
// ==========================================================================
|
|
// AUDIT LOG
|
|
// ==========================================================================
|
|
|
|
/// The org's security audit log, newest first, paginated with `limit` and
|
|
/// `offset`, filtered by `event_type`, `actor`, `from` and `to`. Admin-only,
|
|
/// and refused to API tokens. Returns `event_types` for the filter too.
|
|
static String get auditLog => '$apiBaseUrl/org/audit-log/';
|
|
|
|
// ==========================================================================
|
|
// WEBHOOKS
|
|
// ==========================================================================
|
|
|
|
/// Outbound webhooks. **Admin-only on every route, reads included**: a hook
|
|
/// URL is often a credential of its own and the delivery log holds copies of
|
|
/// records. GET lists endpoints plus the event catalogue; POST creates, and
|
|
/// its response is the only one besides a rotate that carries the secret.
|
|
static String get webhooks => '$apiBaseUrl/webhooks/';
|
|
|
|
/// One endpoint. GET, PATCH (partial) and DELETE; another org's id is 404.
|
|
static String webhook(String id) => '$apiBaseUrl/webhooks/$id/';
|
|
|
|
/// Queue a `ping` delivery to the endpoint.
|
|
static String webhookTest(String id) => '$apiBaseUrl/webhooks/$id/test/';
|
|
|
|
/// Replace the signing secret; the response carries the new one, once.
|
|
static String webhookRotateSecret(String id) =>
|
|
'$apiBaseUrl/webhooks/$id/rotate-secret/';
|
|
|
|
/// The endpoint's delivery log, paginated with `limit` and `offset`.
|
|
static String webhookDeliveries(String id) =>
|
|
'$apiBaseUrl/webhooks/$id/deliveries/';
|
|
|
|
/// Send a settled delivery again, as a new row with the same event id.
|
|
static String webhookRedeliver(String deliveryId) =>
|
|
'$apiBaseUrl/webhooks/deliveries/$deliveryId/redeliver/';
|
|
|
|
// ==========================================================================
|
|
// DASHBOARD
|
|
// ==========================================================================
|
|
|
|
/// Dashboard summary data
|
|
static String get dashboard => '$apiBaseUrl/dashboard/';
|
|
|
|
// ==========================================================================
|
|
// CRM ENDPOINTS
|
|
// ==========================================================================
|
|
|
|
/// Leads management
|
|
static String get leads => '$apiBaseUrl/leads/';
|
|
|
|
/// Lead comment (for update/delete)
|
|
static String leadComment(String commentId) =>
|
|
'$apiBaseUrl/leads/comment/$commentId/';
|
|
|
|
/// The org's active lead pipelines, under `pipelines`. A vertical pack
|
|
/// creates one (education's "Admissions", for instance).
|
|
static String get leadPipelines => '$apiBaseUrl/leads/pipelines/';
|
|
|
|
/// The lead board. With `pipeline_id` it returns that pipeline's stages as
|
|
/// `columns`, plus `unstaged`: the visible leads in no stage yet. Narrowed
|
|
/// server-side to the leads the caller's list shows.
|
|
static String get leadsKanban => '$apiBaseUrl/leads/kanban/';
|
|
|
|
/// Move a lead into a stage (`stage_id`), or out of its pipeline with an
|
|
/// explicit `stage_id: null`. The server checks the caller may edit the lead
|
|
/// and that a lead already in a pipeline stays in it.
|
|
static String leadMove(String id) => '$apiBaseUrl/leads/$id/move/';
|
|
|
|
/// One lead pipeline with its stages. PUT renames, DELETE removes it
|
|
/// (refused while it holds leads). Writes are admin-only server-side.
|
|
static String leadPipeline(String id) => '$apiBaseUrl/leads/pipelines/$id/';
|
|
|
|
/// POST adds a stage to a pipeline, last.
|
|
static String leadPipelineStages(String id) =>
|
|
'$apiBaseUrl/leads/pipelines/$id/stages/';
|
|
|
|
/// POST `stage_ids`: every stage of the pipeline, once, in the new order.
|
|
static String leadPipelineStagesReorder(String id) =>
|
|
'$apiBaseUrl/leads/pipelines/$id/stages/reorder/';
|
|
|
|
/// One lead stage. PUT edits, DELETE removes it (refused while it has leads).
|
|
static String leadStage(String id) => '$apiBaseUrl/leads/stages/$id/';
|
|
|
|
/// Contacts management
|
|
static String get contacts => '$apiBaseUrl/contacts/';
|
|
|
|
/// Accounts (companies) management
|
|
static String get accounts => '$apiBaseUrl/accounts/';
|
|
|
|
/// Opportunities (deals) management
|
|
static String get opportunities => '$apiBaseUrl/opportunities/';
|
|
|
|
/// G19 duplicates, for `leads`, `contacts` and `accounts`: the create-form
|
|
/// check (a POST body), one record's possible duplicates, and the merge
|
|
/// of `{"merge_id": <loser>}` into the record in the URL.
|
|
static String duplicatesCheck(String module) =>
|
|
'$apiBaseUrl/$module/duplicates/';
|
|
static String recordDuplicates(String module, String id) =>
|
|
'$apiBaseUrl/$module/$id/duplicates/';
|
|
static String mergeRecord(String module, String keeperId) =>
|
|
'$apiBaseUrl/$module/$keeperId/merge/';
|
|
static String recordDetail(String module, String id) =>
|
|
'$apiBaseUrl/$module/$id/';
|
|
|
|
/// CSV exports of the six record lists. Each takes its list's own query
|
|
/// string and answers every matching row as a file, not one page.
|
|
static String get leadsExport => '${leads}export/';
|
|
static String get contactsExport => '${contacts}export/';
|
|
static String get accountsExport => '${accounts}export/';
|
|
static String get opportunitiesExport => '${opportunities}export/';
|
|
static String get ticketsExport => '${tickets}export/';
|
|
static String get invoicesExport => '${invoices}export/';
|
|
|
|
/// The caller's own saved list views (G29), for the same six lists.
|
|
static String get savedViews => '$apiBaseUrl/saved-views/';
|
|
static String savedView(String id) => '$apiBaseUrl/saved-views/$id/';
|
|
|
|
/// Opportunity comment (for update/delete)
|
|
static String opportunityComment(String commentId) =>
|
|
'$apiBaseUrl/opportunities/comment/$commentId/';
|
|
|
|
/// The deals board: stage columns with their cards, capped at 100 a column.
|
|
/// Status-based, so a column's `id` is the stage value itself.
|
|
static String get opportunitiesKanban => '$apiBaseUrl/opportunities/kanban/';
|
|
|
|
/// Move a deal to another column and, optionally, to a position in it. Takes
|
|
/// `column_id` plus `above_id`/`below_id`, the same contract the web board
|
|
/// uses. Distinct from `PATCH /opportunities/<id>/`, which edits the deal:
|
|
/// this one also stamps the close when the destination is a closed stage.
|
|
static String opportunityMove(String id) =>
|
|
'$apiBaseUrl/opportunities/$id/move/';
|
|
|
|
/// Every deal pipeline of the org with its stages, under `pipelines`,
|
|
/// default first. Any member reads; POST (`name`) creates one seeded with
|
|
/// the six default stages, admin only.
|
|
static String get dealPipelines => '$apiBaseUrl/opportunities/pipelines/';
|
|
|
|
/// One deal pipeline. PATCH renames it, DELETE removes it (refused for the
|
|
/// default pipeline and for one that still holds deals). Admin only.
|
|
static String dealPipeline(String id) =>
|
|
'$apiBaseUrl/opportunities/pipelines/$id/';
|
|
|
|
/// POST adds a stage, last. Answers with the whole pipeline.
|
|
static String dealPipelineStages(String id) =>
|
|
'$apiBaseUrl/opportunities/pipelines/$id/stages/';
|
|
|
|
/// POST `stage_ids`: every stage of the pipeline, once, in the new order.
|
|
static String dealPipelineStagesReorder(String id) =>
|
|
'$apiBaseUrl/opportunities/pipelines/$id/stages/reorder/';
|
|
|
|
/// One deal stage. PATCH edits it, DELETE removes it (refused while it
|
|
/// holds deals, or when it is the pipeline's last stage of its kind).
|
|
static String dealStage(String id) => '$apiBaseUrl/opportunities/stages/$id/';
|
|
|
|
/// Sales goals. Reading is open to any member and narrowed server-side to
|
|
/// their own goals and their teams'; creating, editing and deleting are
|
|
/// admin-only and answer 403 to everyone else.
|
|
static String get goals => '$apiBaseUrl/opportunities/goals/';
|
|
|
|
static String goal(String id) => '$apiBaseUrl/opportunities/goals/$id/';
|
|
|
|
/// Current individual goals for one period, ranked on attainment. Takes
|
|
/// `?period_type=`, defaulting to MONTHLY server-side.
|
|
///
|
|
/// Narrowed by the same rule as the list. It used to scope nothing, so a
|
|
/// member whose own list came back empty could still read every colleague's
|
|
/// target, attainment and email address off this endpoint.
|
|
static String get goalsLeaderboard =>
|
|
'$apiBaseUrl/opportunities/goals/leaderboard/';
|
|
|
|
/// Finished goal periods with what each one attained, newest first. Grouped
|
|
/// by period and goal type, and narrowed server-side to the goals the caller
|
|
/// may see, the same rule the list and the leaderboard use.
|
|
static String get goalsHistory => '$apiBaseUrl/opportunities/goals/history/';
|
|
|
|
/// Shared files. The list answers two separately paginated envelopes,
|
|
/// `documents_active` and `documents_inactive`. Uploading is open to any
|
|
/// member; editing and deleting are the uploader or an admin.
|
|
static String get documents => '$apiBaseUrl/documents/';
|
|
|
|
static String document(String id) => '$apiBaseUrl/documents/$id/';
|
|
|
|
/// The bytes of a document, gated by the same read predicate as the record.
|
|
///
|
|
/// Fetch this with the Authorization header, never `launchUrl`. The external
|
|
/// browser carries no token, and the file's own `/media/` path is not an
|
|
/// alternative: it is served with no per-file authorization at all, which is
|
|
/// why this endpoint exists.
|
|
static String documentDownload(String id) =>
|
|
'$apiBaseUrl/documents/$id/download/';
|
|
|
|
/// The bytes of a file attached to a lead, deal, ticket or task. Same rule
|
|
/// as `documentDownload`: authenticated fetch, never a storage URL.
|
|
static String attachmentDownload(String id) =>
|
|
'$apiBaseUrl/attachments/$id/download/';
|
|
|
|
// Custom objects. Keys and ids are checked here because they go into the
|
|
// path; a malformed one throws instead of reaching another route.
|
|
static final _objectKey = RegExp(r'^[a-z][a-z0-9_]{1,27}$');
|
|
static bool isObjectKey(String key) => _objectKey.hasMatch(key);
|
|
|
|
static final _fieldKey = RegExp(r'^[a-z][a-z0-9_]{0,63}$');
|
|
static final _uuid = RegExp(
|
|
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
|
caseSensitive: false,
|
|
);
|
|
|
|
static bool isUuid(String id) => _uuid.hasMatch(id);
|
|
|
|
static String _checked(RegExp pattern, String value, String what) {
|
|
if (!pattern.hasMatch(value)) throw ArgumentError.value(value, what);
|
|
return value;
|
|
}
|
|
|
|
static String get objects => '$apiBaseUrl/objects/';
|
|
|
|
static String object(String key) =>
|
|
'$apiBaseUrl/objects/${_checked(_objectKey, key, 'key')}/';
|
|
|
|
static String objectFields(String key) => '${object(key)}fields/';
|
|
|
|
static String objectField(String key, String fieldKey) =>
|
|
'${objectFields(key)}${_checked(_fieldKey, fieldKey, 'fieldKey')}/';
|
|
|
|
static String objectRecords(String key) => '${object(key)}records/';
|
|
|
|
static String objectRecord(String key, String id) =>
|
|
'${objectRecords(key)}${_checked(_uuid, id, 'id')}/';
|
|
|
|
static String objectRecordAttachments(String key, String id) =>
|
|
'${objectRecord(key, id)}attachments/';
|
|
|
|
static String objectRecordAttachment(
|
|
String key,
|
|
String id,
|
|
String attachmentId,
|
|
) =>
|
|
'${objectRecordAttachments(key, id)}'
|
|
'${_checked(_uuid, attachmentId, 'attachmentId')}/';
|
|
|
|
static String get orgModules => '$apiBaseUrl/org/modules/';
|
|
|
|
static String get objectsRelated => '$apiBaseUrl/objects/related/';
|
|
|
|
static String get automationCatalogue => '$apiBaseUrl/automation/catalogue/';
|
|
|
|
static String get automationRules => '$apiBaseUrl/automation/rules/';
|
|
|
|
static String get automationReorder => '${automationRules}reorder/';
|
|
|
|
static String automationRule(String id) =>
|
|
'$automationRules${_checked(_uuid, id, 'id')}/';
|
|
|
|
static String automationRuleTry(String id) => '${automationRule(id)}try/';
|
|
|
|
static String automationRuleRuns(String id, {int offset = 0}) =>
|
|
'${automationRule(id)}runs/?limit=20&offset=$offset';
|
|
|
|
/// Tasks management
|
|
static String get tasks => '$apiBaseUrl/tasks/';
|
|
|
|
/// Tickets (support tickets) management
|
|
static String get tickets => '$apiBaseUrl/cases/';
|
|
|
|
/// Bulk-update selected tickets: POST `{ids, fields}`. Backend answers a
|
|
/// per-record results envelope (`{error, updated, results}`), not a single
|
|
/// ticket, so callers must not treat this like `updateTicket`.
|
|
static String get casesBulkUpdate => '$apiBaseUrl/cases/bulk/update/';
|
|
|
|
/// Bulk-delete selected tickets: POST `{ids}`. Same per-record envelope
|
|
/// shape as [casesBulkUpdate], with `deleted` in place of `updated`.
|
|
static String get casesBulkDelete => '$apiBaseUrl/cases/bulk/delete/';
|
|
|
|
/// CSV import, two POSTs of the same multipart `file`: preview validates
|
|
/// without writing, commit re-validates and creates. [moduleUrl] is
|
|
/// [contacts], [tickets] or [leads]; each gates on `can_mass_import`.
|
|
static String csvImportPreview(String moduleUrl) =>
|
|
'${moduleUrl}import/preview/';
|
|
static String csvImportCommit(String moduleUrl) =>
|
|
'${moduleUrl}import/commit/';
|
|
|
|
/// BottleCRM product support tickets opened by the signed-in user.
|
|
static String get supportTickets => '$apiBaseUrl/support/';
|
|
|
|
static String supportTicket(String id) => '$apiBaseUrl/support/$id/';
|
|
|
|
static String supportTicketReplies(String id) =>
|
|
'$apiBaseUrl/support/$id/replies/';
|
|
|
|
static String supportMessageAttachment(String messageId) =>
|
|
'$apiBaseUrl/support/messages/$messageId/attachment/';
|
|
|
|
/// Ticket detail (retrieve / update / delete / add comment)
|
|
static String ticketDetail(String id) => '$apiBaseUrl/cases/$id/';
|
|
|
|
/// Ticket comment (single, for delete or status checks)
|
|
static String ticketComment(String commentId) =>
|
|
'$apiBaseUrl/cases/comment/$commentId/';
|
|
|
|
/// Watch / unwatch a ticket (POST / DELETE).
|
|
static String ticketWatch(String id) => '$apiBaseUrl/cases/$id/watch/';
|
|
|
|
/// List watchers on a ticket.
|
|
static String ticketWatchers(String id) => '$apiBaseUrl/cases/$id/watchers/';
|
|
|
|
/// Tickets the current user is watching.
|
|
static String get ticketsWatching => '$apiBaseUrl/cases/watching/';
|
|
|
|
/// Merge source ticket as duplicate INTO another ticket.
|
|
static String ticketMerge(String sourceId, String intoId) =>
|
|
'$apiBaseUrl/cases/$sourceId/merge/$intoId/';
|
|
|
|
/// The tickets [id] may be merged into, computed by the server (`?search=`
|
|
/// narrows by name). 403 when the caller may not merge [id] at all.
|
|
static String ticketMergeTargets(String id) =>
|
|
'$apiBaseUrl/cases/$id/merge-targets/';
|
|
|
|
/// Reverse a prior merge. Called on the SOURCE id.
|
|
static String ticketUnmerge(String sourceId) =>
|
|
'$apiBaseUrl/cases/$sourceId/unmerge/';
|
|
|
|
/// The ticket board: status lanes, or one pipeline's stages with
|
|
/// `?pipeline_id=`.
|
|
static String get casesKanban => '$apiBaseUrl/cases/kanban/';
|
|
|
|
/// The org's active ticket pipelines, under `pipelines`.
|
|
static String get casePipelines => '$apiBaseUrl/cases/pipelines/';
|
|
|
|
/// PATCH `{status}` or `{stage_id}`: move a ticket to another board lane.
|
|
static String ticketMove(String id) => '$apiBaseUrl/cases/$id/move/';
|
|
|
|
/// Parent/child tree rooted at the closest visible ancestor.
|
|
static String ticketTree(String id) => '$apiBaseUrl/cases/$id/tree/';
|
|
|
|
/// Link this ticket to a parent (or detach by sending null parent_id).
|
|
static String ticketLinkParent(String id) => '$apiBaseUrl/cases/$id/link/';
|
|
|
|
/// Close this ticket and optionally its descendants.
|
|
static String ticketCloseWithChildren(String id) =>
|
|
'$apiBaseUrl/cases/$id/close-with-children/';
|
|
|
|
/// Time entries for a ticket (GET list / POST manual entry).
|
|
static String ticketTimeEntries(String id) =>
|
|
'$apiBaseUrl/cases/$id/time-entries/';
|
|
|
|
/// Start a running timer on a ticket.
|
|
static String ticketTimerStart(String id) =>
|
|
'$apiBaseUrl/cases/$id/time-entries/start/';
|
|
|
|
/// Time summary for a ticket, totals + per-profile breakdown.
|
|
static String ticketTimeSummary(String id) =>
|
|
'$apiBaseUrl/cases/$id/time-summary/';
|
|
|
|
/// Stop a running timer by entry id (entry-scoped, not case-scoped).
|
|
static String timeEntryStop(String entryId) =>
|
|
'$apiBaseUrl/time-entries/$entryId/stop/';
|
|
|
|
/// A week of the caller's own time, grouped into day buckets.
|
|
///
|
|
/// `start` and `end` are inclusive YYYY-MM-DD. The endpoint also takes a
|
|
/// `profile` parameter, which this app never sends: passing anyone else's is
|
|
/// admin-only and answers 403, and the screen is "your week" by design.
|
|
static String timesheet({required String start, required String end}) =>
|
|
'$apiBaseUrl/time-entries/timesheet/?start=$start&end=$end';
|
|
|
|
/// Where the time went over a window: totals by agent, ticket or account.
|
|
///
|
|
/// `groupBy` is one of agent/ticket/account and `billable`, when given, is
|
|
/// the string 'true' or 'false'; the API rejects anything else rather than
|
|
/// guessing, so the caller passes what the picker chose and nothing else.
|
|
///
|
|
/// The CSV export beside this endpoint is deliberately not wired up here.
|
|
/// Ticket analytics made the same call: a download belongs on the web, and
|
|
/// a phone has nowhere useful to put a spreadsheet.
|
|
static String timeReport({
|
|
required String start,
|
|
required String end,
|
|
required String groupBy,
|
|
String? billable,
|
|
}) {
|
|
final query = StringBuffer(
|
|
'start=$start&end=$end&group_by=${Uri.encodeQueryComponent(groupBy)}',
|
|
);
|
|
if (billable != null) {
|
|
query.write('&billable=${Uri.encodeQueryComponent(billable)}');
|
|
}
|
|
return '$apiBaseUrl/time-entries/report/?$query';
|
|
}
|
|
|
|
/// Solutions (Knowledge Base). List/create.
|
|
static String get solutions => '$apiBaseUrl/cases/solutions/';
|
|
|
|
/// Solution detail (GET / PUT / DELETE).
|
|
static String solutionDetail(String id) => '$apiBaseUrl/cases/solutions/$id/';
|
|
|
|
/// Publish a solution (must be status=approved).
|
|
static String solutionPublish(String id) =>
|
|
'$apiBaseUrl/cases/solutions/$id/publish/';
|
|
|
|
/// Unpublish a solution.
|
|
static String solutionUnpublish(String id) =>
|
|
'$apiBaseUrl/cases/solutions/$id/unpublish/';
|
|
|
|
/// Link a solution to a ticket (POST {solution_id}).
|
|
static String ticketSolutionLink(String ticketId) =>
|
|
'$apiBaseUrl/cases/$ticketId/solutions/';
|
|
|
|
/// Unlink a solution from a ticket.
|
|
static String ticketSolutionUnlink(String ticketId, String solutionId) =>
|
|
'$apiBaseUrl/cases/$ticketId/solutions/$solutionId/';
|
|
|
|
/// Solution suggestions for a ticket (typeahead by ticket context).
|
|
static String ticketSolutionSuggestions(String ticketId) =>
|
|
'$apiBaseUrl/cases/$ticketId/solution-suggestions/';
|
|
|
|
/// Approvals inbox / per-case list. Query params: ?case=&state=&mine=
|
|
static String get approvals => '$apiBaseUrl/cases/approvals/';
|
|
|
|
/// Approve / reject / cancel an approval.
|
|
static String approvalApprove(String id) =>
|
|
'$apiBaseUrl/cases/approvals/$id/approve/';
|
|
static String approvalReject(String id) =>
|
|
'$apiBaseUrl/cases/approvals/$id/reject/';
|
|
static String approvalCancel(String id) =>
|
|
'$apiBaseUrl/cases/approvals/$id/cancel/';
|
|
|
|
/// Request approval for a ticket.
|
|
static String ticketRequestApproval(String ticketId) =>
|
|
'$apiBaseUrl/cases/$ticketId/request-approval/';
|
|
|
|
/// Analytics endpoints. All accept ?from=YYYY-MM-DD&to=YYYY-MM-DD plus
|
|
/// optional ?priority=, ?agent=, ?team= filters.
|
|
static String get analyticsFrt => '$apiBaseUrl/cases/analytics/frt/';
|
|
static String get analyticsMttr => '$apiBaseUrl/cases/analytics/mttr/';
|
|
static String get analyticsBacklog => '$apiBaseUrl/cases/analytics/backlog/';
|
|
static String get analyticsAgents => '$apiBaseUrl/cases/analytics/agents/';
|
|
static String get analyticsSla => '$apiBaseUrl/cases/analytics/sla/';
|
|
static String get analyticsNrt => '$apiBaseUrl/cases/analytics/nrt/';
|
|
|
|
/// CSAT average, count and 1 to 5 distribution. Same filters as analytics;
|
|
/// the window is when the customer answered.
|
|
static String get csatAggregate => '$apiBaseUrl/cases/csat/aggregate/';
|
|
|
|
/// Invoices management.
|
|
///
|
|
/// The list takes `search`, `status`, `account`, `contact`, `opportunity`,
|
|
/// `assigned_to`, `created_by`, `issue_date_gte`/`_lte`, `due_date_gte`/`_lte`
|
|
/// and `sort` (one of created_at, due_date, issue_date, total_amount, status,
|
|
/// optionally `-` prefixed). It answers `{count, next, previous, results,
|
|
/// totals}`, where `totals` is deliberately computed over everything the
|
|
/// caller can see rather than over the current filter or page.
|
|
static String get invoices => '$apiBaseUrl/invoices/';
|
|
|
|
static String invoice(String id) => '$apiBaseUrl/invoices/$id/';
|
|
|
|
/// Emails the invoice to its client and stamps `sent_at`. A Draft becomes
|
|
/// Sent; the server refuses a Paid or Cancelled invoice with a 400.
|
|
static String invoiceSend(String id) => '$apiBaseUrl/invoices/$id/send/';
|
|
|
|
/// Records a payment. Despite the name it takes a partial `amount`, and
|
|
/// defaults to the whole outstanding balance when none is given. The server
|
|
/// rejects an amount that is zero, negative, or above `amount_due`.
|
|
static String invoiceMarkPaid(String id) =>
|
|
'$apiBaseUrl/invoices/$id/mark-paid/';
|
|
|
|
/// Cancels the invoice. Refused with a 400 when it is already Cancelled, or
|
|
/// when it is Paid.
|
|
static String invoiceCancel(String id) => '$apiBaseUrl/invoices/$id/cancel/';
|
|
|
|
/// Payments already recorded against one invoice.
|
|
static String invoicePayments(String id) =>
|
|
'$apiBaseUrl/invoices/$id/payments/';
|
|
|
|
/// Raises a Draft invoice from a won deal, copying its line items. 404 for a
|
|
/// deal the caller may not open; 400 for one not won or with no lines.
|
|
static String invoiceFromDeal(String dealId) =>
|
|
'$apiBaseUrl/invoices/from-opportunity/$dealId/';
|
|
|
|
/// Estimates (quotes). Takes `search`, `status` and `account`.
|
|
static String get estimates => '$apiBaseUrl/invoices/estimates/';
|
|
|
|
/// Raises an invoice from the estimate. Refused with a 400 once one exists.
|
|
static String estimateConvert(String id) =>
|
|
'$apiBaseUrl/invoices/estimates/$id/convert/';
|
|
|
|
static String estimateSend(String id) =>
|
|
'$apiBaseUrl/invoices/estimates/$id/send/';
|
|
|
|
/// Recurring billing schedules. Takes `is_active=true|false`.
|
|
static String get recurringInvoices => '$apiBaseUrl/invoices/recurring/';
|
|
|
|
/// Flips a schedule between paused and running. The server toggles from the
|
|
/// stored value rather than taking a target state, so this must never be
|
|
/// sent twice for one intent.
|
|
static String recurringToggle(String id) =>
|
|
'$apiBaseUrl/invoices/recurring/$id/toggle/';
|
|
|
|
/// The product catalogue. Reading is open to any member; creating, editing
|
|
/// and deleting are admin-only and answer 403 otherwise.
|
|
static String get products => '$apiBaseUrl/invoices/products/';
|
|
static String product(String id) => '$apiBaseUrl/invoices/products/$id/';
|
|
|
|
/// PDF templates. Neither of these carries the raw HTML or CSS: only the
|
|
/// admin-only editor endpoint does, and this app does not call it. That is
|
|
/// what lets the phone edit a template at all. The fields it writes are the
|
|
/// ordinary ones, the detail PUT is partial, so a save from here leaves the
|
|
/// markup exactly as the web editor left it.
|
|
static String get invoiceTemplates => '$apiBaseUrl/invoices/templates/';
|
|
static String invoiceTemplate(String id) =>
|
|
'$apiBaseUrl/invoices/templates/$id/';
|
|
|
|
/// Reports. Both are admin-only and answer 403 to everyone else.
|
|
static String get invoiceReportDashboard =>
|
|
'$apiBaseUrl/invoices/reports/dashboard/';
|
|
static String get invoiceReportAging => '$apiBaseUrl/invoices/reports/aging/';
|
|
|
|
/// An account's statement for a period, as figures and as a PDF. Admin-only
|
|
/// like the reports. Blank dates are left out, so the server's default (this
|
|
/// month so far, in the org's day) applies.
|
|
static String accountStatement(
|
|
String accountId, {
|
|
String? start,
|
|
String? end,
|
|
}) => _withPeriod('$apiBaseUrl/invoices/statements/$accountId/', start, end);
|
|
static String accountStatementPdf(
|
|
String accountId, {
|
|
String? start,
|
|
String? end,
|
|
}) => _withPeriod(
|
|
'$apiBaseUrl/invoices/statements/$accountId/pdf/',
|
|
start,
|
|
end,
|
|
);
|
|
static String _withPeriod(String url, String? start, String? end) {
|
|
final query = {
|
|
if (start != null && start.isNotEmpty) 'start_date': start,
|
|
if (end != null && end.isNotEmpty) 'end_date': end,
|
|
};
|
|
return query.isEmpty
|
|
? url
|
|
: Uri.parse(url).replace(queryParameters: query).toString();
|
|
}
|
|
|
|
/// Kanban boards. The list returns only boards you own or belong to, so an
|
|
/// empty list is an answer and not a permission failure: creating one makes
|
|
/// you its owner.
|
|
static String get boards => '$apiBaseUrl/boards/';
|
|
|
|
/// A board's lanes, each with its cards nested under `tasks`. This is the
|
|
/// one call that renders a whole board.
|
|
static String boardLanes(String boardId) =>
|
|
'$apiBaseUrl/boards/$boardId/columns/';
|
|
|
|
/// POST a card into a lane. The server takes the lane from this URL, so a
|
|
/// card cannot be posted into another board's lane.
|
|
static String boardLaneCards(String laneId) =>
|
|
'$apiBaseUrl/boards/columns/$laneId/tasks/';
|
|
|
|
/// One card: PUT to move or edit it, DELETE to remove it.
|
|
static String boardCard(String cardId) => '$apiBaseUrl/boards/tasks/$cardId/';
|
|
|
|
// ==========================================================================
|
|
// NOTIFICATIONS
|
|
// ==========================================================================
|
|
|
|
/// The signed-in user's own feed. Every query here is scoped to
|
|
/// `recipient=request.profile` server-side, so there is no id to pass and no
|
|
/// way to ask for someone else's.
|
|
///
|
|
/// `?unread=true` narrows it and `?limit=` caps it at 100. Delivery is by
|
|
/// polling with `?since=<iso>`; there is no stream.
|
|
static String notifications({int limit = 50}) =>
|
|
'$apiBaseUrl/notifications/?limit=$limit';
|
|
|
|
/// Mark one notification read. Idempotent: re-marking does not move the
|
|
/// timestamp backwards.
|
|
static String notificationRead(String id) =>
|
|
'$apiBaseUrl/notifications/$id/read/';
|
|
|
|
/// Mark every unread notification read.
|
|
static String get notificationsReadAll =>
|
|
'$apiBaseUrl/notifications/read-all/';
|
|
|
|
// ==========================================================================
|
|
// USERS & TAGS ENDPOINTS
|
|
// ==========================================================================
|
|
|
|
/// Get teams and users (for assignment dropdowns)
|
|
static String get teamsAndUsers => '$apiBaseUrl/users/get-teams-and-users/';
|
|
|
|
/// Auto-routing rules, ordered by `priority_order` server-side. That order is
|
|
/// the behaviour: the engine takes the first match and, when the rule says
|
|
/// so, stops.
|
|
static String get routingRules => '$apiBaseUrl/cases/routing-rules/';
|
|
|
|
/// One routing rule. PUT is partial, so a body may carry one key. DELETE is a
|
|
/// HARD delete here, unlike custom fields and tags.
|
|
static String routingRule(String id) =>
|
|
'$apiBaseUrl/cases/routing-rules/$id/';
|
|
|
|
/// Tags management. Active tags only by default; the settings screen passes
|
|
/// `?include_archived=true` because it is the one that turns a tag back on
|
|
/// and cannot offer that for a row it never fetched.
|
|
static String get tags => '$apiBaseUrl/tags/';
|
|
|
|
/// One tag. DELETE turns it off (a soft archive: the row and every record's
|
|
/// link to it stay), and nothing hard-deletes a tag anywhere.
|
|
static String tag(String id) => '$apiBaseUrl/tags/$id/';
|
|
|
|
/// Turn an archived tag back on.
|
|
static String tagRestore(String id) => '$apiBaseUrl/tags/$id/restore/';
|
|
|
|
/// Move every record off this tag onto the one named by `into`, then archive
|
|
/// this one. Both ids are re-resolved inside the caller's org server-side.
|
|
static String tagMerge(String id) => '$apiBaseUrl/tags/$id/merge/';
|
|
|
|
/// Escalation policies, at most one per priority. Each row arrives with
|
|
/// `breaches_last_30d`, a server-side count the list has no other way to get.
|
|
static String get escalationPolicies =>
|
|
'$apiBaseUrl/cases/escalation-policies/';
|
|
|
|
/// One escalation policy. PUT is partial, so a one-key body is legal, and it
|
|
/// strips `priority` before the serializer sees it. DELETE is a HARD delete
|
|
/// here, unlike custom fields and tags.
|
|
static String escalationPolicy(String id) =>
|
|
'$apiBaseUrl/cases/escalation-policies/$id/';
|
|
|
|
/// Inbound email addresses. GET is open to any member, and strips the two
|
|
/// admin-only integration fields (`topic_arn`, `has_webhook_secret`) for
|
|
/// everyone else. POST, PUT and DELETE are admin-only.
|
|
static String get mailboxes => '$apiBaseUrl/cases/mailboxes/';
|
|
|
|
/// One mailbox. PUT is partial, so a body may carry one key. DELETE is a HARD
|
|
/// delete and takes the topic pin with it.
|
|
static String mailbox(String id) => '$apiBaseUrl/cases/mailboxes/$id/';
|
|
|
|
/// Approval rules, the gates on a ticket close. GET is open to any member;
|
|
/// every write is admin-only.
|
|
static String get approvalRules => '$apiBaseUrl/cases/approval-rules/';
|
|
|
|
/// One approval rule. PUT is partial. DELETE is two outcomes behind one verb:
|
|
/// a rule with approval history is turned off rather than destroyed, and both
|
|
/// answer 2xx, so the response body is what says which happened.
|
|
static String approvalRule(String id) =>
|
|
'$apiBaseUrl/cases/approval-rules/$id/';
|
|
|
|
/// The org's default business calendar, created on first read. Reading is
|
|
/// open to any member; every write below is admin-only.
|
|
static String get businessCalendar => '$apiBaseUrl/business-hours/calendar/';
|
|
|
|
/// The calendar's own detail url, which is what PUT goes to. The collection
|
|
/// path above does not accept one.
|
|
static String businessCalendarDetail(String id) =>
|
|
'$apiBaseUrl/business-hours/calendar/$id/';
|
|
|
|
/// Holidays on a calendar. POST is idempotent on date: a date already stored
|
|
/// answers 200 with the EXISTING row, name included, and creates nothing.
|
|
static String businessHolidays(String calendarId) =>
|
|
'$apiBaseUrl/business-hours/calendar/$calendarId/holidays/';
|
|
|
|
/// One holiday. DELETE is a hard delete and the day counts as working time
|
|
/// again at once.
|
|
static String businessHoliday(String calendarId, String holidayId) =>
|
|
'$apiBaseUrl/business-hours/calendar/$calendarId/holidays/$holidayId/';
|
|
|
|
/// The org's reopen policy, a singleton created on first read. **GET is
|
|
/// admin-only here**, unlike the business calendar, so the screen that reads
|
|
/// it has to gate on role rather than show a member a 403.
|
|
static String get reopenPolicy => '$apiBaseUrl/cases/reopen-policy/';
|
|
|
|
/// Custom field definitions (per-org schema for entities like Case/Lead/...).
|
|
/// Query with `?target_model=Case&active_only=true`. Pass
|
|
/// `include_counts=false` unless you need `records_missing_value`: computing
|
|
/// it costs a full scan of the org's records per definition.
|
|
static String get customFieldDefinitions => '$apiBaseUrl/custom-fields/';
|
|
|
|
/// Saved replies. GET lists the ones visible to the caller: every org macro
|
|
/// plus their own personal ones, never anybody else's. `?active=true` for
|
|
/// the reply-box picker, unfiltered for the settings screen, which has to
|
|
/// see the turned-off rows to offer turning one back on.
|
|
static String get macros => '$apiBaseUrl/macros/';
|
|
|
|
/// One saved reply. PATCH edits it (PUT runs the serializer non-partial and
|
|
/// 400s without both title and body). DELETE turns an org macro off and
|
|
/// deletes a personal one for good, decided server-side from the row.
|
|
static String macro(String id) => '$apiBaseUrl/macros/$id/';
|
|
|
|
/// Substitute the `%token%` placeholders against a ticket and return the
|
|
/// text: POST `{"case_id": "<ticket id>"}`. Server-side on purpose, so the
|
|
/// supported token set lives in one place and no client can drift from it.
|
|
static String macroRender(String id) => '$apiBaseUrl/macros/$id/render/';
|
|
|
|
/// Apply a macro's actions (status, priority, assignees, tags) to a ticket:
|
|
/// POST `{"case_id": "<ticket id>", "only": [...]}`. The server runs them
|
|
/// through the ticket PATCH's own write, so every gate a PATCH meets applies.
|
|
static String macroApply(String id) => '$apiBaseUrl/macros/$id/apply/';
|
|
|
|
/// One definition: PUT edits it, DELETE turns it off. DELETE is a soft
|
|
/// delete (`CustomFieldDefinitionDetailView.delete` flips `is_active` and
|
|
/// leaves stored values readable), so nothing here says "delete" to a user.
|
|
/// Both verbs are admin-only server-side.
|
|
static String customField(String id) => '$apiBaseUrl/custom-fields/$id/';
|
|
|
|
/// Embeddable web forms (issue #634). GET lists the org's forms and is open
|
|
/// to any member; POST is admin-only, because a published form is an
|
|
/// anonymous endpoint that writes leads into the org and creating one is
|
|
/// closer to minting a credential than to editing a record. The response
|
|
/// carries a `totals` block computed over every form, so a paginated page is
|
|
/// never counted for the summary figures.
|
|
///
|
|
/// The public endpoints under `/api/public/forms/…` are deliberately absent
|
|
/// from this file. They are called by a visitor's browser and take no
|
|
/// credential; a route here would mean this app sending a JWT to an endpoint
|
|
/// that does not want one.
|
|
static String get webForms => '$apiBaseUrl/webforms/';
|
|
|
|
/// One form with its whole field list and both embed snippets. PUT is
|
|
/// partial server-side despite the verb, and replaces the field list
|
|
/// wholesale when `fields` is present. Another org's id answers 404 rather
|
|
/// than 403, so the id space cannot be used to discover which forms exist
|
|
/// elsewhere. Admin-only for PUT and DELETE.
|
|
static String webForm(String id) => '$apiBaseUrl/webforms/$id/';
|
|
|
|
/// Start accepting submissions. Its own endpoint rather than a field on the
|
|
/// update, because it validates the source state (already-published is a
|
|
/// 400, not a no-op) and the form's shape: no email field means no publish,
|
|
/// and a form set to redirect needs a redirect URL. The 400 body carries the
|
|
/// reason, which is the whole useful content of the response.
|
|
static String webFormPublish(String id) =>
|
|
'$apiBaseUrl/webforms/$id/publish/';
|
|
|
|
static String webFormUnpublish(String id) =>
|
|
'$apiBaseUrl/webforms/$id/unpublish/';
|
|
|
|
/// Submissions for one form, newest first, accepted and rejected alike.
|
|
/// `reject_reason` is not in the response on purpose: naming the control
|
|
/// that caught a bot is how the next bot gets past it.
|
|
static String webFormSubmissions(String id) =>
|
|
'$apiBaseUrl/webforms/$id/submissions/';
|
|
|
|
/// Views, submissions and conversion over a fixed trailing 30 days. The
|
|
/// window is not a parameter; the series is zero-filled server-side so a
|
|
/// quiet day is a real zero rather than a gap.
|
|
static String webFormAnalytics(String id) =>
|
|
'$apiBaseUrl/webforms/$id/analytics/';
|
|
|
|
/// People in the org: GET lists active and inactive, POST invites.
|
|
/// Admin-only server-side, 403 for everyone else on both verbs.
|
|
static String get orgUsers => '$apiBaseUrl/users/';
|
|
|
|
/// One person's role: PATCH `{"role": "ADMIN"|"USER"}`. Takes the USER id,
|
|
/// not the profile id.
|
|
static String orgUser(String userId) => '$apiBaseUrl/user/$userId/';
|
|
|
|
/// Activate or deactivate: POST `{"status": "Active"|"Inactive"}`. Also the
|
|
/// USER id. The server refuses to deactivate the last active admin.
|
|
static String orgUserStatus(String userId) =>
|
|
'$apiBaseUrl/user/$userId/status/';
|
|
|
|
// ==========================================================================
|
|
// REQUEST CONFIGURATION
|
|
// ==========================================================================
|
|
|
|
/// Connection timeout duration
|
|
static const Duration connectTimeout = Duration(seconds: 30);
|
|
|
|
/// Uploads get longer. A 25 MB attachment (the server's limit) does not
|
|
/// finish in 30 seconds on a phone connection, and the timeout that fired
|
|
/// would look like a server fault.
|
|
static const Duration uploadTimeout = Duration(minutes: 3);
|
|
|
|
/// Receive timeout duration
|
|
static const Duration receiveTimeout = Duration(seconds: 30);
|
|
|
|
/// Default request headers
|
|
static Map<String, String> get defaultHeaders => {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
};
|
|
}
|