adapty 0.11.0 → 0.13.0
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/README.md +45 -0
- package/dist/cli/base/adapty/flags.d.ts +65 -0
- package/dist/cli/base/adapty/flags.js +95 -0
- package/dist/cli/base/adapty/index.d.ts +1 -0
- package/dist/cli/base/adapty/index.js +1 -0
- package/dist/cli/base/attribution/flags.d.ts +3 -3
- package/dist/cli/base/attribution/flags.js +5 -20
- package/dist/cli/commands/analytics/catalog.d.ts +10 -0
- package/dist/cli/commands/analytics/catalog.js +52 -0
- package/dist/cli/commands/analytics/chart.d.ts +21 -0
- package/dist/cli/commands/analytics/chart.js +47 -0
- package/dist/cli/commands/analytics/cohort.d.ts +19 -0
- package/dist/cli/commands/analytics/cohort.js +40 -0
- package/dist/cli/commands/analytics/conversion.d.ts +20 -0
- package/dist/cli/commands/analytics/conversion.js +44 -0
- package/dist/cli/commands/analytics/funnel.d.ts +16 -0
- package/dist/cli/commands/analytics/funnel.js +32 -0
- package/dist/cli/commands/analytics/ltv.d.ts +19 -0
- package/dist/cli/commands/analytics/ltv.js +40 -0
- package/dist/cli/commands/analytics/retention.d.ts +17 -0
- package/dist/cli/commands/analytics/retention.js +41 -0
- package/dist/cli/commands/analytics/values.d.ts +11 -0
- package/dist/cli/commands/analytics/values.js +33 -0
- package/dist/cli/commands/attribution/report/command.js +6 -11
- package/dist/cli/commands/product-offers/create.d.ts +15 -0
- package/dist/cli/commands/product-offers/create.js +41 -0
- package/dist/cli/commands/product-offers/get.d.ts +13 -0
- package/dist/cli/commands/product-offers/get.js +16 -0
- package/dist/cli/commands/product-offers/list.d.ts +11 -0
- package/dist/cli/commands/product-offers/list.js +15 -0
- package/dist/cli/commands/product-offers/update.d.ts +20 -0
- package/dist/cli/commands/product-offers/update.js +41 -0
- package/dist/cli/errors.js +8 -1
- package/dist/cli/input/app.js +1 -11
- package/dist/cli/input/filter.d.ts +11 -0
- package/dist/cli/input/filter.js +16 -0
- package/dist/cli/input/offer.d.ts +3 -0
- package/dist/cli/input/offer.js +9 -0
- package/dist/cli/input/period.d.ts +10 -0
- package/dist/cli/input/period.js +23 -0
- package/dist/cli/input/product.d.ts +3 -0
- package/dist/cli/input/product.js +9 -0
- package/dist/cli/input/uuid.d.ts +6 -0
- package/dist/cli/input/uuid.js +11 -0
- package/dist/cli/views/analytics/csv.d.ts +3 -0
- package/dist/cli/views/analytics/csv.js +16 -0
- package/dist/cli/views/analytics/human.d.ts +41 -0
- package/dist/cli/views/analytics/human.js +134 -0
- package/dist/cli/views/analytics/segments.d.ts +68 -0
- package/dist/cli/views/analytics/segments.js +189 -0
- package/dist/commands/analytics/catalog.d.ts +1 -0
- package/dist/commands/analytics/catalog.js +2 -0
- package/dist/commands/analytics/chart.d.ts +1 -0
- package/dist/commands/analytics/chart.js +2 -0
- package/dist/commands/analytics/cohort.d.ts +1 -0
- package/dist/commands/analytics/cohort.js +2 -0
- package/dist/commands/analytics/conversion.d.ts +1 -0
- package/dist/commands/analytics/conversion.js +2 -0
- package/dist/commands/analytics/funnel.d.ts +1 -0
- package/dist/commands/analytics/funnel.js +2 -0
- package/dist/commands/analytics/ltv.d.ts +1 -0
- package/dist/commands/analytics/ltv.js +2 -0
- package/dist/commands/analytics/retention.d.ts +1 -0
- package/dist/commands/analytics/retention.js +2 -0
- package/dist/commands/analytics/values.d.ts +1 -0
- package/dist/commands/analytics/values.js +2 -0
- package/dist/commands/product-offers/create.d.ts +1 -0
- package/dist/commands/product-offers/create.js +2 -0
- package/dist/commands/product-offers/get.d.ts +1 -0
- package/dist/commands/product-offers/get.js +2 -0
- package/dist/commands/product-offers/list.d.ts +1 -0
- package/dist/commands/product-offers/list.js +2 -0
- package/dist/commands/product-offers/update.d.ts +1 -0
- package/dist/commands/product-offers/update.js +2 -0
- package/dist/lib/api-schemas.d.ts +1 -0
- package/dist/sdk/adapty/analytics/catalog.d.ts +51 -0
- package/dist/sdk/adapty/analytics/catalog.js +5 -0
- package/dist/sdk/adapty/analytics/chart.d.ts +41 -0
- package/dist/sdk/adapty/analytics/chart.js +6 -0
- package/dist/sdk/adapty/analytics/cohort.d.ts +79 -0
- package/dist/sdk/adapty/analytics/cohort.js +4 -0
- package/dist/sdk/adapty/analytics/conversion.d.ts +55 -0
- package/dist/sdk/adapty/analytics/conversion.js +7 -0
- package/dist/sdk/adapty/analytics/funnel.d.ts +41 -0
- package/dist/sdk/adapty/analytics/funnel.js +3 -0
- package/dist/sdk/adapty/analytics/index.d.ts +24 -0
- package/dist/sdk/adapty/analytics/index.js +13 -0
- package/dist/sdk/adapty/analytics/ltv.d.ts +27 -0
- package/dist/sdk/adapty/analytics/ltv.js +4 -0
- package/dist/sdk/adapty/analytics/model.d.ts +53 -0
- package/dist/sdk/adapty/analytics/model.js +12 -0
- package/dist/sdk/adapty/analytics/query.d.ts +48 -0
- package/dist/sdk/adapty/analytics/query.js +52 -0
- package/dist/sdk/adapty/analytics/renewal-periods.d.ts +23 -0
- package/dist/sdk/adapty/analytics/renewal-periods.js +27 -0
- package/dist/sdk/adapty/analytics/resource.d.ts +29 -0
- package/dist/sdk/adapty/analytics/resource.js +72 -0
- package/dist/sdk/adapty/analytics/retention.d.ts +32 -0
- package/dist/sdk/adapty/analytics/retention.js +6 -0
- package/dist/sdk/adapty/analytics/values.d.ts +20 -0
- package/dist/sdk/adapty/analytics/values.js +1 -0
- package/dist/sdk/adapty/index.d.ts +8 -0
- package/dist/sdk/adapty/index.js +6 -0
- package/dist/sdk/adapty/product-offers/create.d.ts +5 -0
- package/dist/sdk/adapty/product-offers/create.js +12 -0
- package/dist/sdk/adapty/product-offers/index.d.ts +9 -0
- package/dist/sdk/adapty/product-offers/index.js +5 -0
- package/dist/sdk/adapty/product-offers/model.d.ts +48 -0
- package/dist/sdk/adapty/product-offers/model.js +30 -0
- package/dist/sdk/adapty/product-offers/resource.d.ts +21 -0
- package/dist/sdk/adapty/product-offers/resource.js +28 -0
- package/dist/sdk/adapty/product-offers/update.d.ts +19 -0
- package/dist/sdk/adapty/product-offers/update.js +42 -0
- package/dist/sdk/attribution/index.d.ts +1 -1
- package/dist/sdk/attribution/index.js +1 -1
- package/dist/sdk/attribution/report.d.ts +0 -2
- package/dist/sdk/attribution/report.js +2 -26
- package/dist/sdk/core/dates.d.ts +15 -0
- package/dist/sdk/core/dates.js +34 -0
- package/dist/sdk/core/errors.d.ts +3 -0
- package/oclif.manifest.json +1163 -128
- package/package.json +7 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Issue } from '../../core/errors.js';
|
|
2
|
+
/** The values one filter takes for an app: what `filters.<dimension>` accepts, and the `key` of its groups. */
|
|
3
|
+
export type FilterValuesInput = {
|
|
4
|
+
appId: string;
|
|
5
|
+
/** A filter key, as the catalog `dimensions` lists it; the backend refuses any other. */
|
|
6
|
+
dimension: string;
|
|
7
|
+
};
|
|
8
|
+
/** `group` is the parent in the dashboard list (the region of a country, ...); null at the top level. */
|
|
9
|
+
export type FilterValue = {
|
|
10
|
+
group: null | string;
|
|
11
|
+
label: string;
|
|
12
|
+
value: string;
|
|
13
|
+
};
|
|
14
|
+
export type FilterValuesResponse = {
|
|
15
|
+
data: FilterValue[];
|
|
16
|
+
meta: {
|
|
17
|
+
dimension: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare const validateFilterValues: (input: FilterValuesInput) => Issue[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const validateFilterValues = (input) => (input.dimension.trim() === '' ? [{ message: 'must not be empty', path: 'dimension' }] : []);
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import type { AccessLevelsApi } from './access-levels.js';
|
|
2
|
+
import type { AnalyticsApi } from './analytics/index.js';
|
|
2
3
|
import type { AppsApi } from './apps/index.js';
|
|
3
4
|
import type { AuthApi } from './auth/index.js';
|
|
4
5
|
import type { FeedbackApi } from './feedback/index.js';
|
|
5
6
|
import type { MigrationApi } from './migrations/index.js';
|
|
7
|
+
import type { ProductOffersApi } from './product-offers/index.js';
|
|
6
8
|
import type { Clock } from '../core/clock.js';
|
|
7
9
|
import type { RetryAttempt } from '../core/http/index.js';
|
|
10
|
+
export { periodTypes, periodUnits, revenueBases, validateChart, validateCohort, validateConversion, validateFilterValues, validateFunnel, validateLtv, validateRetention, } from './analytics/index.js';
|
|
8
11
|
export { platforms } from './apps/index.js';
|
|
12
|
+
export { offerTypes } from './product-offers/index.js';
|
|
9
13
|
export { developerErrorParser } from './errors.js';
|
|
10
14
|
export { sentiments } from './feedback/index.js';
|
|
11
15
|
export type { AccessLevel, AccessLevelList, AccessLevelsApi } from './access-levels.js';
|
|
16
|
+
export type { AnalyticsFilter, AnalyticsSegment, CatalogChart, CatalogOperation, CatalogOption, CatalogResponse, CatalogSegmentation, ChartInput, ChartResponse, CohortInput, CohortResponse, ConversionInput, ConversionResponse, FilterValue, FilterValuesInput, FilterValuesResponse, FunnelInput, FunnelResponse, LtvInput, LtvResponse, PeriodUnit, RetentionInput, RetentionResponse, } from './analytics/index.js';
|
|
12
17
|
export type { AppDetail, AppsApi, AppSummary, CreateAppInput, UpdateAppInput } from './apps/index.js';
|
|
13
18
|
export type { AuthApi, AuthUser, IssuedToken } from './auth/index.js';
|
|
14
19
|
export type { FeedbackApi, SendFeedbackInput, Sentiment } from './feedback/index.js';
|
|
15
20
|
export type { Action, ActionKind, AvailableFlow, CreateMigrationInput, Envelope, Issue, JsonSchema, Migration, MigrationApi, MigrationList, MigrationState, Progress, ResourceRef, Step, StepStatus, WaitOptions, WizardError, } from './migrations/index.js';
|
|
21
|
+
export type { CreateProductOfferInput, OfferType, ProductOffer, ProductOffersApi, StoreOffer, UpdateProductOfferInput, } from './product-offers/index.js';
|
|
16
22
|
export type { PageParams, Paginated, Pagination } from './pagination.js';
|
|
17
23
|
/** Exported because the adapter compares the resolved URL with it and warns when they differ. */
|
|
18
24
|
export declare const DEFAULT_ADAPTY_API_URL = "https://api-admin.adapty.io/api/v1/developer";
|
|
@@ -36,10 +42,12 @@ export type AdaptyOptions = {
|
|
|
36
42
|
};
|
|
37
43
|
export type Adapty = {
|
|
38
44
|
accessLevels: AccessLevelsApi;
|
|
45
|
+
analytics: AnalyticsApi;
|
|
39
46
|
apps: AppsApi;
|
|
40
47
|
auth: AuthApi;
|
|
41
48
|
feedback: FeedbackApi;
|
|
42
49
|
migrations: MigrationApi;
|
|
50
|
+
productOffers: ProductOffersApi;
|
|
43
51
|
};
|
|
44
52
|
/** The assembly point of the developer API: one transport, resources on top of it. */
|
|
45
53
|
export declare const createAdapty: (options?: AdaptyOptions) => Adapty;
|
package/dist/sdk/adapty/index.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { systemClock } from '../core/clock.js';
|
|
2
2
|
import { createHttp } from '../core/http/index.js';
|
|
3
3
|
import { accessLevels } from './access-levels.js';
|
|
4
|
+
import { analytics } from './analytics/index.js';
|
|
4
5
|
import { apps } from './apps/index.js';
|
|
5
6
|
import { auth } from './auth/index.js';
|
|
6
7
|
import { developerErrorParser } from './errors.js';
|
|
7
8
|
import { feedback } from './feedback/index.js';
|
|
8
9
|
import { migrations } from './migrations/index.js';
|
|
10
|
+
import { productOffers } from './product-offers/index.js';
|
|
11
|
+
export { periodTypes, periodUnits, revenueBases, validateChart, validateCohort, validateConversion, validateFilterValues, validateFunnel, validateLtv, validateRetention, } from './analytics/index.js';
|
|
9
12
|
export { platforms } from './apps/index.js';
|
|
13
|
+
export { offerTypes } from './product-offers/index.js';
|
|
10
14
|
export { developerErrorParser } from './errors.js';
|
|
11
15
|
export { sentiments } from './feedback/index.js';
|
|
12
16
|
/** Exported because the adapter compares the resolved URL with it and warns when they differ. */
|
|
@@ -46,11 +50,13 @@ export const createAdapty = (options = {}) => {
|
|
|
46
50
|
const wizard = createHttp({ ...transport, trailingSlash: false });
|
|
47
51
|
return {
|
|
48
52
|
accessLevels: accessLevels(http),
|
|
53
|
+
analytics: analytics(http),
|
|
49
54
|
apps: apps(http),
|
|
50
55
|
auth: auth(http),
|
|
51
56
|
feedback: feedback(http),
|
|
52
57
|
// The clock is a dependency here too, not only in the transport: `waitFor` sleeps
|
|
53
58
|
// between polls, and a test must be able to do that instantly.
|
|
54
59
|
migrations: migrations(wizard, options.clock ?? systemClock),
|
|
60
|
+
productOffers: productOffers(http),
|
|
55
61
|
};
|
|
56
62
|
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { OfferWrite } from './model.js';
|
|
2
|
+
import type { Issue } from '../../core/errors.js';
|
|
3
|
+
export type CreateProductOfferInput = OfferWrite;
|
|
4
|
+
/** A list of problems, knowing nothing about flags or the network. A path names an input field. */
|
|
5
|
+
export declare const validateCreateProductOffer: (input: CreateProductOfferInput) => Issue[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { hasStoreId, winBackIssues } from './model.js';
|
|
2
|
+
/** A list of problems, knowing nothing about flags or the network. A path names an input field. */
|
|
3
|
+
export const validateCreateProductOffer = (input) => {
|
|
4
|
+
const issues = [];
|
|
5
|
+
if (input.title.trim() === '') {
|
|
6
|
+
issues.push({ message: 'must not be empty', path: 'title' });
|
|
7
|
+
}
|
|
8
|
+
if (!hasStoreId(input)) {
|
|
9
|
+
issues.push({ fields: ['iosOfferId', 'androidOfferId'], message: 'pass {iosOfferId}, {androidOfferId} or both' });
|
|
10
|
+
}
|
|
11
|
+
return [...issues, ...winBackIssues(input)];
|
|
12
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The door of the product-offers resource: re-exports only, no code of its own. */
|
|
2
|
+
export { offerTypes } from './model.js';
|
|
3
|
+
export { validateCreateProductOffer } from './create.js';
|
|
4
|
+
export { productOffers } from './resource.js';
|
|
5
|
+
export { validateUpdateProductOffer } from './update.js';
|
|
6
|
+
export type { OfferType, ProductOffer, StoreOffer } from './model.js';
|
|
7
|
+
export type { CreateProductOfferInput } from './create.js';
|
|
8
|
+
export type { ProductOffersApi } from './resource.js';
|
|
9
|
+
export type { UpdateProductOfferInput } from './update.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** The door of the product-offers resource: re-exports only, no code of its own. */
|
|
2
|
+
export { offerTypes } from './model.js';
|
|
3
|
+
export { validateCreateProductOffer } from './create.js';
|
|
4
|
+
export { productOffers } from './resource.js';
|
|
5
|
+
export { validateUpdateProductOffer } from './update.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every operation of the resource shares: the offer as the server sends it, and the type
|
|
3
|
+
* vocabulary. Answers pass through exactly as they arrive, in snake_case, which is what --json prints.
|
|
4
|
+
*/
|
|
5
|
+
import type { Issue } from '../../core/errors.js';
|
|
6
|
+
/** The types a write accepts, as the server names them. */
|
|
7
|
+
export declare const offerTypes: readonly ["promotional", "win_back"];
|
|
8
|
+
export type OfferType = (typeof offerTypes)[number];
|
|
9
|
+
export type StoreOffer = {
|
|
10
|
+
offer_id: string;
|
|
11
|
+
};
|
|
12
|
+
export type ProductOffer = {
|
|
13
|
+
/** Null when the offer has no fixed duration. */
|
|
14
|
+
duration: null | string;
|
|
15
|
+
id: string;
|
|
16
|
+
/** Not the OfferType union: the server also answers `introductory`, which a write refuses. */
|
|
17
|
+
offer_type: string;
|
|
18
|
+
product_id: string;
|
|
19
|
+
/** Keyed as the server stored it: `app_store`, `play_store`, and any other key it may hold. */
|
|
20
|
+
store_offers: Record<string, StoreOffer>;
|
|
21
|
+
title: string;
|
|
22
|
+
};
|
|
23
|
+
/** The fields of a write. */
|
|
24
|
+
export type OfferWrite = {
|
|
25
|
+
androidOfferId?: string | undefined;
|
|
26
|
+
iosOfferId?: string | undefined;
|
|
27
|
+
offerType: OfferType;
|
|
28
|
+
title: string;
|
|
29
|
+
};
|
|
30
|
+
/** An update merged onto the stored offer, before the rules have checked its type. */
|
|
31
|
+
export type OfferDraft = Omit<OfferWrite, 'offerType'> & {
|
|
32
|
+
offerType: string;
|
|
33
|
+
};
|
|
34
|
+
export declare const isOfferType: (value: string) => value is OfferType;
|
|
35
|
+
/** Trimmed, and a blank id is not an id: `--ios-offer-id ""` counts as absent. */
|
|
36
|
+
export declare const storeId: (value: string | undefined) => string | undefined;
|
|
37
|
+
export declare const hasStoreId: (write: OfferDraft) => boolean;
|
|
38
|
+
/** Google Play has no win-back offers: the server refuses a win_back with an android id. */
|
|
39
|
+
export declare const winBackIssues: (write: OfferDraft) => Issue[];
|
|
40
|
+
type OfferWriteRequest = {
|
|
41
|
+
android_offer_id?: string;
|
|
42
|
+
ios_offer_id?: string;
|
|
43
|
+
offer_type: string;
|
|
44
|
+
title: string;
|
|
45
|
+
};
|
|
46
|
+
/** The body of create and update alike: the server replaces the whole offer. Blank ids are left out. */
|
|
47
|
+
export declare const toWriteRequest: (write: OfferDraft) => OfferWriteRequest;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every operation of the resource shares: the offer as the server sends it, and the type
|
|
3
|
+
* vocabulary. Answers pass through exactly as they arrive, in snake_case, which is what --json prints.
|
|
4
|
+
*/
|
|
5
|
+
/** The types a write accepts, as the server names them. */
|
|
6
|
+
export const offerTypes = ['promotional', 'win_back'];
|
|
7
|
+
export const isOfferType = (value) => offerTypes.includes(value);
|
|
8
|
+
/** Trimmed, and a blank id is not an id: `--ios-offer-id ""` counts as absent. */
|
|
9
|
+
export const storeId = (value) => {
|
|
10
|
+
const trimmed = value?.trim();
|
|
11
|
+
return trimmed === '' ? undefined : trimmed;
|
|
12
|
+
};
|
|
13
|
+
export const hasStoreId = (write) => storeId(write.iosOfferId) !== undefined || storeId(write.androidOfferId) !== undefined;
|
|
14
|
+
/** Google Play has no win-back offers: the server refuses a win_back with an android id. */
|
|
15
|
+
export const winBackIssues = (write) => (write.offerType === 'win_back' && storeId(write.androidOfferId) !== undefined
|
|
16
|
+
? [{ message: 'a win_back offer takes only an App Store offer id', path: 'androidOfferId' }]
|
|
17
|
+
: []);
|
|
18
|
+
/** The body of create and update alike: the server replaces the whole offer. Blank ids are left out. */
|
|
19
|
+
export const toWriteRequest = (write) => {
|
|
20
|
+
const body = { offer_type: write.offerType, title: write.title };
|
|
21
|
+
const ios = storeId(write.iosOfferId);
|
|
22
|
+
const android = storeId(write.androidOfferId);
|
|
23
|
+
if (ios !== undefined) {
|
|
24
|
+
body.ios_offer_id = ios;
|
|
25
|
+
}
|
|
26
|
+
if (android !== undefined) {
|
|
27
|
+
body.android_offer_id = android;
|
|
28
|
+
}
|
|
29
|
+
return body;
|
|
30
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CreateProductOfferInput } from './create.js';
|
|
2
|
+
import type { ProductOffer } from './model.js';
|
|
3
|
+
import type { UpdateProductOfferInput } from './update.js';
|
|
4
|
+
import type { Http } from '../../core/http/index.js';
|
|
5
|
+
import type { Paginated } from '../pagination.js';
|
|
6
|
+
/**
|
|
7
|
+
* Offers are listed and created under a product and read and changed by their own id under the app:
|
|
8
|
+
* the two path shapes the server routes. There is no delete: offers are deleted in the dashboard only.
|
|
9
|
+
*
|
|
10
|
+
* The validating methods are async on purpose: a broken rule arrives as a rejected promise, like a
|
|
11
|
+
* network failure.
|
|
12
|
+
*/
|
|
13
|
+
export declare const productOffers: (http: Http) => {
|
|
14
|
+
get: (appId: string, offerId: string) => Promise<ProductOffer>;
|
|
15
|
+
/** The server answers every offer of the product on one page. */
|
|
16
|
+
list: (appId: string, productId: string) => Promise<Paginated<ProductOffer>>;
|
|
17
|
+
create: (appId: string, productId: string, input: CreateProductOfferInput) => Promise<ProductOffer>;
|
|
18
|
+
/** A full replace on the server, so this reads the stored offer and sends it back merged. */
|
|
19
|
+
update: (appId: string, offerId: string, input: UpdateProductOfferInput) => Promise<ProductOffer>;
|
|
20
|
+
};
|
|
21
|
+
export type ProductOffersApi = ReturnType<typeof productOffers>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { assertValid } from '../../core/validation.js';
|
|
2
|
+
import { validateCreateProductOffer } from './create.js';
|
|
3
|
+
import { toWriteRequest } from './model.js';
|
|
4
|
+
import { mergeProductOffer, validateMergedProductOffer, validateUpdateProductOffer } from './update.js';
|
|
5
|
+
/**
|
|
6
|
+
* Offers are listed and created under a product and read and changed by their own id under the app:
|
|
7
|
+
* the two path shapes the server routes. There is no delete: offers are deleted in the dashboard only.
|
|
8
|
+
*
|
|
9
|
+
* The validating methods are async on purpose: a broken rule arrives as a rejected promise, like a
|
|
10
|
+
* network failure.
|
|
11
|
+
*/
|
|
12
|
+
export const productOffers = (http) => ({
|
|
13
|
+
get: (appId, offerId) => http.get(`/apps/${appId}/offers/${offerId}`),
|
|
14
|
+
/** The server answers every offer of the product on one page. */
|
|
15
|
+
list: (appId, productId) => http.get(`/apps/${appId}/products/${productId}/offers`),
|
|
16
|
+
create: async (appId, productId, input) => {
|
|
17
|
+
assertValid(validateCreateProductOffer(input));
|
|
18
|
+
return http.post(`/apps/${appId}/products/${productId}/offers`, toWriteRequest(input));
|
|
19
|
+
},
|
|
20
|
+
/** A full replace on the server, so this reads the stored offer and sends it back merged. */
|
|
21
|
+
update: async (appId, offerId, input) => {
|
|
22
|
+
assertValid(validateUpdateProductOffer(input));
|
|
23
|
+
const stored = await http.get(`/apps/${appId}/offers/${offerId}`);
|
|
24
|
+
const merged = mergeProductOffer(stored, input);
|
|
25
|
+
assertValid(validateMergedProductOffer(stored, input, merged));
|
|
26
|
+
return http.put(`/apps/${appId}/offers/${offerId}`, toWriteRequest(merged));
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { OfferDraft, OfferType, ProductOffer } from './model.js';
|
|
2
|
+
import type { Issue } from '../../core/errors.js';
|
|
3
|
+
/** A store id is replaced by a string and removed by null; undefined or blank keeps what is stored. */
|
|
4
|
+
export type UpdateProductOfferInput = {
|
|
5
|
+
androidOfferId?: null | string | undefined;
|
|
6
|
+
iosOfferId?: null | string | undefined;
|
|
7
|
+
offerType?: OfferType | undefined;
|
|
8
|
+
title?: string | undefined;
|
|
9
|
+
};
|
|
10
|
+
/** Rules that need no stored offer, so they run before the first request. */
|
|
11
|
+
export declare const validateUpdateProductOffer: (input: UpdateProductOfferInput) => Issue[];
|
|
12
|
+
/** The offer after the update: the server replaces the whole offer, so untouched fields come from the stored one. */
|
|
13
|
+
export declare const mergeProductOffer: (offer: ProductOffer, input: UpdateProductOfferInput) => OfferDraft;
|
|
14
|
+
/**
|
|
15
|
+
* Rules on the stored offer and the merged one, so they run after the read and before the write. The
|
|
16
|
+
* write carries only `app_store` and `play_store`, so an offer with any other store key is refused
|
|
17
|
+
* rather than losing that side to the replace.
|
|
18
|
+
*/
|
|
19
|
+
export declare const validateMergedProductOffer: (offer: ProductOffer, input: UpdateProductOfferInput, merged: OfferDraft) => Issue[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { hasStoreId, isOfferType, storeId, winBackIssues } from './model.js';
|
|
2
|
+
const given = (value) => (value === null ? null : storeId(value));
|
|
3
|
+
/** Rules that need no stored offer, so they run before the first request. */
|
|
4
|
+
export const validateUpdateProductOffer = (input) => {
|
|
5
|
+
const fields = [input.title, input.offerType, given(input.iosOfferId), given(input.androidOfferId)];
|
|
6
|
+
if (fields.every(value => value === undefined)) {
|
|
7
|
+
return [{ message: 'nothing to update: pass a title, a type or a store offer id' }];
|
|
8
|
+
}
|
|
9
|
+
if (input.title?.trim() === '') {
|
|
10
|
+
return [{ message: 'must not be empty', path: 'title' }];
|
|
11
|
+
}
|
|
12
|
+
return [];
|
|
13
|
+
};
|
|
14
|
+
const pick = (value, stored) => (value === undefined ? stored : (value ?? undefined));
|
|
15
|
+
/** The offer after the update: the server replaces the whole offer, so untouched fields come from the stored one. */
|
|
16
|
+
export const mergeProductOffer = (offer, input) => ({
|
|
17
|
+
androidOfferId: pick(given(input.androidOfferId), storeId(offer.store_offers.play_store?.offer_id)),
|
|
18
|
+
iosOfferId: pick(given(input.iosOfferId), storeId(offer.store_offers.app_store?.offer_id)),
|
|
19
|
+
offerType: input.offerType ?? offer.offer_type,
|
|
20
|
+
title: input.title ?? offer.title,
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Rules on the stored offer and the merged one, so they run after the read and before the write. The
|
|
24
|
+
* write carries only `app_store` and `play_store`, so an offer with any other store key is refused
|
|
25
|
+
* rather than losing that side to the replace.
|
|
26
|
+
*/
|
|
27
|
+
export const validateMergedProductOffer = (offer, input, merged) => {
|
|
28
|
+
if (!isOfferType(offer.offer_type)) {
|
|
29
|
+
return [{ message: `${offer.offer_type} offers are read-only` }];
|
|
30
|
+
}
|
|
31
|
+
const otherKeys = Object.keys(offer.store_offers).filter(key => key !== 'app_store' && key !== 'play_store');
|
|
32
|
+
if (otherKeys.length > 0) {
|
|
33
|
+
return [{ message: `this offer has store sides the update would drop (${otherKeys.join(', ')}): edit it in the dashboard` }];
|
|
34
|
+
}
|
|
35
|
+
if (!hasStoreId(merged)) {
|
|
36
|
+
return [{ message: 'an offer needs at least one store offer id: delete it in the dashboard instead of clearing both' }];
|
|
37
|
+
}
|
|
38
|
+
if (given(input.androidOfferId) === undefined && winBackIssues(merged).length > 0) {
|
|
39
|
+
return [{ message: 'a win_back offer takes only an App Store offer id: clear the stored Google Play one' }];
|
|
40
|
+
}
|
|
41
|
+
return winBackIssues(merged);
|
|
42
|
+
};
|
|
@@ -2,7 +2,7 @@ import type { AttributionApi } from './resource.js';
|
|
|
2
2
|
import type { Clock } from '../core/clock.js';
|
|
3
3
|
import type { RetryAttempt } from '../core/http/index.js';
|
|
4
4
|
export { attributionErrorParser } from './errors.js';
|
|
5
|
-
export { granularities,
|
|
5
|
+
export { granularities, reportGroupBy, revenueBases, sortDirections, validateReport } from './report.js';
|
|
6
6
|
export { validateValues } from './values.js';
|
|
7
7
|
export type { Dimension, DimensionCatalog, DimensionIdentity, Metric, MetricCatalog, MetricUnit, ReportLimits, } from './catalog.js';
|
|
8
8
|
export type { Granularity, ReportData, ReportFilter, ReportGroupBy, ReportInput, ReportMeta, ReportRow, ReportSort, RevenueBasis, SortDirection, } from './report.js';
|
|
@@ -3,7 +3,7 @@ import { createHttp, defaultRetryPolicy, defaultShouldRetry } from '../core/http
|
|
|
3
3
|
import { attributionErrorParser } from './errors.js';
|
|
4
4
|
import { attribution } from './resource.js';
|
|
5
5
|
export { attributionErrorParser } from './errors.js';
|
|
6
|
-
export { granularities,
|
|
6
|
+
export { granularities, reportGroupBy, revenueBases, sortDirections, validateReport } from './report.js';
|
|
7
7
|
export { validateValues } from './values.js';
|
|
8
8
|
/** Exported because the adapter compares the resolved URL with it and warns when they differ. */
|
|
9
9
|
export const DEFAULT_ATTRIBUTION_API_URL = 'https://api-ua.adapty.io/api/v1/cli';
|
|
@@ -57,8 +57,6 @@ export type ReportMeta = {
|
|
|
57
57
|
/** The query as the backend resolved it: timezone, currency and defaults filled in. */
|
|
58
58
|
query: Record<string, unknown>;
|
|
59
59
|
};
|
|
60
|
-
/** The `YYYY-MM-DD` shape alone, shared with the adapter's date flag parser. */
|
|
61
|
-
export declare const ISO_DATE: RegExp;
|
|
62
60
|
/** Shared with values: both read a day range, and both are refused the same way. */
|
|
63
61
|
export declare const validateDateRange: (input: {
|
|
64
62
|
dateFrom: string;
|
|
@@ -1,35 +1,11 @@
|
|
|
1
|
+
import { validateDayRange } from '../core/dates.js';
|
|
1
2
|
/** In the order the backend documents them, so a flag's help reads the same way. */
|
|
2
3
|
export const reportGroupBy = ['date', 'campaign', 'adset', 'ad', 'keyword', 'channel', 'country', 'store'];
|
|
3
4
|
export const granularities = ['day', 'week', 'month', 'quarter', 'year'];
|
|
4
5
|
export const revenueBases = ['gross', 'proceeds', 'net'];
|
|
5
6
|
export const sortDirections = ['asc', 'desc'];
|
|
6
|
-
/** The `YYYY-MM-DD` shape alone, shared with the adapter's date flag parser. */
|
|
7
|
-
export const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
8
|
-
/** The shape alone lets 2026-02-30 through; a round trip through Date does not. */
|
|
9
|
-
const isIsoDate = (value) => {
|
|
10
|
-
if (!ISO_DATE.test(value)) {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
const date = new Date(`${value}T00:00:00Z`);
|
|
14
|
-
return !Number.isNaN(date.getTime()) && date.toISOString().startsWith(value);
|
|
15
|
-
};
|
|
16
7
|
/** Shared with values: both read a day range, and both are refused the same way. */
|
|
17
|
-
export const validateDateRange = (input) => {
|
|
18
|
-
const issues = [];
|
|
19
|
-
const fromValid = isIsoDate(input.dateFrom);
|
|
20
|
-
const toValid = isIsoDate(input.dateTo);
|
|
21
|
-
if (!fromValid) {
|
|
22
|
-
issues.push({ message: 'must be a date in YYYY-MM-DD form', path: 'dateFrom' });
|
|
23
|
-
}
|
|
24
|
-
if (!toValid) {
|
|
25
|
-
issues.push({ message: 'must be a date in YYYY-MM-DD form', path: 'dateTo' });
|
|
26
|
-
}
|
|
27
|
-
// ISO days order as strings, but only two real days are worth comparing
|
|
28
|
-
if (fromValid && toValid && input.dateTo < input.dateFrom) {
|
|
29
|
-
issues.push({ message: 'must not be before the start date', path: 'dateTo' });
|
|
30
|
-
}
|
|
31
|
-
return issues;
|
|
32
|
-
};
|
|
8
|
+
export const validateDateRange = (input) => validateDayRange(input.dateFrom, input.dateTo, { from: 'dateFrom', to: 'dateTo' });
|
|
33
9
|
/**
|
|
34
10
|
* The rules a report can break without asking the server. Which metric and dimension names exist
|
|
35
11
|
* is the catalog's knowledge, so the backend checks those. An Issue path names an input field
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Issue } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Days as the backends take them: `YYYY-MM-DD`, an inclusive range of two. Shared by every product that
|
|
4
|
+
* reads a day range, so a range is checked, and refused, in the same words everywhere.
|
|
5
|
+
*/
|
|
6
|
+
/** The `YYYY-MM-DD` shape alone, shared with the adapter's date flag parser. */
|
|
7
|
+
export declare const ISO_DATE: RegExp;
|
|
8
|
+
/**
|
|
9
|
+
* Both ends real days, the end not before the start. `paths` names the two input fields, so the adapter
|
|
10
|
+
* can name the two flags the user typed.
|
|
11
|
+
*/
|
|
12
|
+
export declare const validateDayRange: (from: string, to: string, paths: {
|
|
13
|
+
from: string;
|
|
14
|
+
to: string;
|
|
15
|
+
}) => Issue[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Days as the backends take them: `YYYY-MM-DD`, an inclusive range of two. Shared by every product that
|
|
3
|
+
* reads a day range, so a range is checked, and refused, in the same words everywhere.
|
|
4
|
+
*/
|
|
5
|
+
/** The `YYYY-MM-DD` shape alone, shared with the adapter's date flag parser. */
|
|
6
|
+
export const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
7
|
+
/** The shape alone lets 2026-02-30 through; a round trip through Date does not. */
|
|
8
|
+
const isIsoDate = (value) => {
|
|
9
|
+
if (!ISO_DATE.test(value)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const date = new Date(`${value}T00:00:00Z`);
|
|
13
|
+
return !Number.isNaN(date.getTime()) && date.toISOString().startsWith(value);
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Both ends real days, the end not before the start. `paths` names the two input fields, so the adapter
|
|
17
|
+
* can name the two flags the user typed.
|
|
18
|
+
*/
|
|
19
|
+
export const validateDayRange = (from, to, paths) => {
|
|
20
|
+
const issues = [];
|
|
21
|
+
const fromValid = isIsoDate(from);
|
|
22
|
+
const toValid = isIsoDate(to);
|
|
23
|
+
if (!fromValid) {
|
|
24
|
+
issues.push({ message: 'must be a date in YYYY-MM-DD form', path: paths.from });
|
|
25
|
+
}
|
|
26
|
+
if (!toValid) {
|
|
27
|
+
issues.push({ message: 'must be a date in YYYY-MM-DD form', path: paths.to });
|
|
28
|
+
}
|
|
29
|
+
// ISO days order as strings, but only two real days are worth comparing
|
|
30
|
+
if (fromValid && toValid && to < from) {
|
|
31
|
+
issues.push({ message: 'must not be before the start date', path: paths.to });
|
|
32
|
+
}
|
|
33
|
+
return issues;
|
|
34
|
+
};
|
|
@@ -44,6 +44,9 @@ export declare class CancelledError extends SdkError {
|
|
|
44
44
|
constructor();
|
|
45
45
|
}
|
|
46
46
|
export type Issue = {
|
|
47
|
+
/** The other input fields that `message` names, each written there as `{fieldName}`. */
|
|
48
|
+
fields?: readonly string[] | undefined;
|
|
49
|
+
/** Plain words; another input field appears as `{fieldName}`, listed in `fields`, for the adapter to write. */
|
|
47
50
|
message: string;
|
|
48
51
|
/** The input field the problem belongs to. No path: the input as a whole is wrong. */
|
|
49
52
|
path?: string | undefined;
|