| 1 | import * as androidpublisher from "@distilled.cloud/gcp/androidpublisher_v3"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import { createPhysicalName } from "../../PhysicalName.ts"; |
| 4 | |
| 5 | export const DEFAULT_LANGUAGE = "en-US"; |
| 6 | export const DEFAULT_REGION = "US"; |
| 7 | export const DEFAULT_CURRENCY = "USD"; |
| 8 | export const DEFAULT_REGIONS_VERSION = "2025/01"; |
| 9 | export const DEFAULT_PRICE_MICROS = "990000"; |
| 10 | export const DEFAULT_SUBSCRIPTION_PRICE_UNITS = "5"; |
| 11 | export const DEFAULT_BILLING_PERIOD = "P1M"; |
| 12 | export const DEFAULT_OFFER_DURATION = "P1W"; |
| 13 | export const MAX_PRODUCT_ID_LENGTH = 40; |
| 14 | export const MAX_BASE_PLAN_ID_LENGTH = 63; |
| 15 | export const MAX_OFFER_ID_LENGTH = 63; |
| 16 | export const MAX_SKU_LENGTH = 40; |
| 17 | export const MAX_LISTING_TITLE_LENGTH = 50; |
| 18 | |
| 19 | export const sameText = (left: string | undefined, right: string | undefined) => |
| 20 | (left ?? "") === (right ?? ""); |
| 21 | |
| 22 | export const jsonEqual = (left: unknown, right: unknown) => |
| 23 | JSON.stringify(left ?? null) === JSON.stringify(right ?? null); |
| 24 | |
| 25 | export const sameStringList = ( |
| 26 | left: readonly string[] | undefined, |
| 27 | right: readonly string[] | undefined, |
| 28 | ) => |
| 29 | jsonEqual( |
| 30 | [...(left ?? [])].slice().sort(), |
| 31 | [...(right ?? [])].slice().sort(), |
| 32 | ); |
| 33 | |
| 34 | export const updateMaskOf = (...fields: Array<string | undefined>) => |
| 35 | fields.filter((field): field is string => field !== undefined).join(","); |
| 36 | |
| 37 | const isMissing = <E extends { readonly _tag: string }>( |
| 38 | error: E, |
| 39 | ): error is Extract<E, { readonly _tag: "NotFound" }> => |
| 40 | error._tag === "NotFound"; |
| 41 | |
| 42 | export const catchMissing = <A, E extends { readonly _tag: string }, R>( |
| 43 | effect: Effect.Effect<A, E, R>, |
| 44 | ) => effect.pipe(Effect.catchIf(isMissing, () => Effect.succeed(undefined))); |
| 45 | |
| 46 | export const ignoreMissing = <E extends { readonly _tag: string }, R>( |
| 47 | effect: Effect.Effect<unknown, E, R>, |
| 48 | ) => effect.pipe(Effect.catchIf(isMissing, () => Effect.void)); |
| 49 | |
| 50 | const toGenerated = ( |
| 51 | id: string, |
| 52 | requested: string | undefined, |
| 53 | existing: string | undefined, |
| 54 | options: { |
| 55 | maxLength: number; |
| 56 | delimiter: string; |
| 57 | prefixIfNeeded: string; |
| 58 | }, |
| 59 | ) => |
| 60 | Effect.gen(function* () { |
| 61 | if (requested !== undefined && requested.length > 0) return requested; |
| 62 | if (existing !== undefined && existing.length > 0) return existing; |
| 63 | const generated = yield* createPhysicalName({ |
| 64 | id, |
| 65 | maxLength: options.maxLength, |
| 66 | lowercase: true, |
| 67 | delimiter: options.delimiter, |
| 68 | }); |
| 69 | const next = /^[a-z]/.test(generated) |
| 70 | ? generated |
| 71 | : `${options.prefixIfNeeded}${generated}`.slice(0, options.maxLength); |
| 72 | const trimmed = next |
| 73 | .replace(new RegExp(`${options.delimiter}+`, "g"), options.delimiter) |
| 74 | .replace( |
| 75 | new RegExp(`^${options.delimiter}|${options.delimiter}$`, "g"), |
| 76 | "", |
| 77 | ); |
| 78 | return trimmed.length >= 1 |
| 79 | ? trimmed |
| 80 | : `${options.prefixIfNeeded}1`.slice(0, options.maxLength); |
| 81 | }); |
| 82 | |
| 83 | export const toProductId = ( |
| 84 | id: string, |
| 85 | requested: string | undefined, |
| 86 | existing: string | undefined, |
| 87 | ) => |
| 88 | toGenerated(id, requested, existing, { |
| 89 | maxLength: MAX_PRODUCT_ID_LENGTH, |
| 90 | delimiter: "_", |
| 91 | prefixIfNeeded: "a", |
| 92 | }); |
| 93 | |
| 94 | export const toSku = ( |
| 95 | id: string, |
| 96 | requested: string | undefined, |
| 97 | existing: string | undefined, |
| 98 | ) => |
| 99 | toGenerated(id, requested, existing, { |
| 100 | maxLength: MAX_SKU_LENGTH, |
| 101 | delimiter: "_", |
| 102 | prefixIfNeeded: "a", |
| 103 | }); |
| 104 | |
| 105 | export const toBasePlanId = ( |
| 106 | id: string, |
| 107 | requested: string | undefined, |
| 108 | existing: string | undefined, |
| 109 | ) => |
| 110 | toGenerated(id, requested, existing, { |
| 111 | maxLength: MAX_BASE_PLAN_ID_LENGTH, |
| 112 | delimiter: "-", |
| 113 | prefixIfNeeded: "b", |
| 114 | }); |
| 115 | |
| 116 | export const toOfferId = ( |
| 117 | id: string, |
| 118 | requested: string | undefined, |
| 119 | existing: string | undefined, |
| 120 | ) => |
| 121 | toGenerated(id, requested, existing, { |
| 122 | maxLength: MAX_OFFER_ID_LENGTH, |
| 123 | delimiter: "-", |
| 124 | prefixIfNeeded: "o", |
| 125 | }); |
| 126 | |
| 127 | export const toDisplayName = ( |
| 128 | id: string, |
| 129 | requested: string | undefined, |
| 130 | existing: string | undefined, |
| 131 | maxLength = MAX_LISTING_TITLE_LENGTH, |
| 132 | ) => |
| 133 | Effect.gen(function* () { |
| 134 | if (requested !== undefined && requested.length > 0) { |
| 135 | return requested.slice(0, maxLength); |
| 136 | } |
| 137 | if (existing !== undefined && existing.length > 0) { |
| 138 | return existing.slice(0, maxLength); |
| 139 | } |
| 140 | return (yield* toProductId(id, undefined, undefined)).slice(0, maxLength); |
| 141 | }); |
| 142 | |
| 143 | export const defaultSubscriptionListings = ( |
| 144 | listings: readonly androidpublisher.SubscriptionListing[] | undefined, |
| 145 | title: string, |
| 146 | ): androidpublisher.SubscriptionListing[] => |
| 147 | listings && listings.length > 0 |
| 148 | ? listings.map((listing) => ({ |
| 149 | ...listing, |
| 150 | languageCode: listing.languageCode ?? DEFAULT_LANGUAGE, |
| 151 | })) |
| 152 | : [{ languageCode: DEFAULT_LANGUAGE, title }]; |
| 153 | |
| 154 | export const defaultInappListings = ( |
| 155 | listings: androidpublisher.InAppProductListingMap | undefined, |
| 156 | title: string, |
| 157 | defaultLanguage: string, |
| 158 | ): androidpublisher.InAppProductListingMap => { |
| 159 | const next = { ...(listings ?? {}) }; |
| 160 | const current = next[defaultLanguage] ?? { title }; |
| 161 | next[defaultLanguage] = { ...current, title: current.title ?? title }; |
| 162 | return next; |
| 163 | }; |
| 164 | |
| 165 | export const publicBasePlans = ( |
| 166 | plans: readonly androidpublisher.BasePlan[] | undefined, |
| 167 | ) => |
| 168 | plans?.map((plan) => ({ |
| 169 | regionalConfigs: plan.regionalConfigs, |
| 170 | otherRegionsConfig: plan.otherRegionsConfig, |
| 171 | offerTags: plan.offerTags, |
| 172 | installmentsBasePlanType: plan.installmentsBasePlanType, |
| 173 | autoRenewingBasePlanType: plan.autoRenewingBasePlanType, |
| 174 | basePlanId: plan.basePlanId, |
| 175 | prepaidBasePlanType: plan.prepaidBasePlanType, |
| 176 | })); |
| 177 | |
| 178 | export const defaultOfferPhases = |
| 179 | (): androidpublisher.SubscriptionOfferPhase[] => [ |
| 180 | { |
| 181 | duration: DEFAULT_OFFER_DURATION, |
| 182 | recurrenceCount: 1, |
| 183 | regionalConfigs: [{ regionCode: DEFAULT_REGION, free: {} }], |
| 184 | }, |
| 185 | ]; |
| 186 | |
| 187 | export const defaultOfferRegionalConfigs = |
| 188 | (): androidpublisher.RegionalSubscriptionOfferConfig[] => [ |
| 189 | { regionCode: DEFAULT_REGION, newSubscriberAvailability: true }, |
| 190 | ]; |
| 191 | |
| 192 | export const defaultOfferTargeting = |
| 193 | (): androidpublisher.SubscriptionOfferTargeting => ({ |
| 194 | acquisitionRule: { scope: { thisSubscription: {} } }, |
| 195 | }); |
| 196 | |
| 197 | export const defaultInappPrice = (): androidpublisher.Price => ({ |
| 198 | currency: DEFAULT_CURRENCY, |
| 199 | priceMicros: DEFAULT_PRICE_MICROS, |
| 200 | }); |
| 201 | |
| 202 | export const getSubscription = (packageName: string, productId: string) => |
| 203 | packageName.length === 0 || productId.length === 0 |
| 204 | ? Effect.succeed(undefined) |
| 205 | : catchMissing( |
| 206 | androidpublisher.getMonetizationSubscriptions({ |
| 207 | packageName, |
| 208 | productId, |
| 209 | }), |
| 210 | ); |
| 211 | |
| 212 | export const getOffer = ( |
| 213 | packageName: string, |
| 214 | productId: string, |
| 215 | basePlanId: string, |
| 216 | offerId: string, |
| 217 | ) => |
| 218 | packageName.length === 0 || |
| 219 | productId.length === 0 || |
| 220 | basePlanId.length === 0 || |
| 221 | offerId.length === 0 |
| 222 | ? Effect.succeed(undefined) |
| 223 | : catchMissing( |
| 224 | androidpublisher.getMonetizationSubscriptionsBasePlansOffers({ |
| 225 | packageName, |
| 226 | productId, |
| 227 | basePlanId, |
| 228 | offerId, |
| 229 | }), |
| 230 | ); |
| 231 | |
| 232 | export const getInappproduct = (packageName: string, sku: string) => |
| 233 | packageName.length === 0 || sku.length === 0 |
| 234 | ? Effect.succeed(undefined) |
| 235 | : catchMissing(androidpublisher.getInappproducts({ packageName, sku })); |
| 236 | |
| 237 | export const getEdit = (packageName: string, editId: string) => |
| 238 | packageName.length === 0 || editId.length === 0 |
| 239 | ? Effect.succeed(undefined) |
| 240 | : catchMissing(androidpublisher.getEdits({ packageName, editId })); |