@explorins/pers-sdk-react-native 2.3.6 → 2.3.8
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/LICENSE +21 -0
- package/README.md +1266 -1244
- package/dist/__tests__/PersSDKProvider.test.js +33 -0
- package/dist/hooks/useBusiness.d.ts +2 -2
- package/dist/hooks/useBusiness.d.ts.map +1 -1
- package/dist/hooks/useBusiness.js +2 -2
- package/dist/hooks/useIPFSUrl.d.ts +3 -3
- package/dist/hooks/useIPFSUrl.d.ts.map +1 -1
- package/dist/hooks/useIPFSUrl.js +7 -7
- package/dist/hooks/useTransactions.d.ts.map +1 -1
- package/dist/hooks/useTransactions.js +13 -5
- package/dist/hooks/useWeb3.js +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2412 -492
- package/dist/index.js.map +1 -1
- package/dist/providers/PersSDKProvider.d.ts +4 -0
- package/dist/providers/PersSDKProvider.d.ts.map +1 -1
- package/dist/providers/PersSDKProvider.js +14 -0
- package/dist/providers/react-native-http-client.d.ts +2 -0
- package/dist/providers/react-native-http-client.d.ts.map +1 -1
- package/dist/providers/react-native-http-client.js +5 -2
- package/dist/storage/rn-secure-storage.d.ts.map +1 -1
- package/dist/storage/rn-secure-storage.js +21 -8
- package/package.json +3 -3
- package/src/__tests__/PersSDKProvider.test.tsx +484 -420
- package/src/__tests__/setup.ts +40 -40
- package/src/hooks/index.ts +60 -60
- package/src/hooks/useAnalytics.ts +327 -327
- package/src/hooks/useAuth.ts +265 -265
- package/src/hooks/useBookings.ts +191 -191
- package/src/hooks/useBusiness.ts +251 -251
- package/src/hooks/useCampaigns.ts +500 -500
- package/src/hooks/useDonations.ts +66 -66
- package/src/hooks/useEvents.ts +372 -372
- package/src/hooks/useFiles.ts +155 -155
- package/src/hooks/useIPFSUrl.ts +192 -192
- package/src/hooks/usePurchases.ts +65 -65
- package/src/hooks/useRedemptions.ts +467 -467
- package/src/hooks/useTenants.ts +72 -72
- package/src/hooks/useTokenBalances.ts +385 -385
- package/src/hooks/useTokens.ts +313 -313
- package/src/hooks/useTransactionSigner.ts +611 -611
- package/src/hooks/useTransactions.ts +299 -290
- package/src/hooks/useTriggerSources.ts +301 -301
- package/src/hooks/useUserStatus.ts +61 -61
- package/src/hooks/useUsers.ts +167 -167
- package/src/hooks/useWeb3.ts +355 -355
- package/src/index.ts +777 -774
- package/src/polyfills/index.ts +160 -160
- package/src/polyfills/web3-polyfills.ts +97 -97
- package/src/providers/PersSDKProvider.tsx +393 -374
- package/src/providers/react-native-auth-provider.ts +110 -110
- package/src/providers/react-native-http-client.ts +150 -145
- package/src/providers/rn-dpop-provider.ts +190 -190
- package/src/storage/async-storage-token-storage.ts +185 -185
- package/src/storage/rn-secure-storage.ts +182 -170
- package/src/types/react-native-globals.d.ts +45 -45
- package/src/utils/index.ts +8 -8
- package/src/utils/platform-detection.ts +86 -86
- package/dist/index-CzLxqePZ.js +0 -7717
- package/dist/index-CzLxqePZ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -2607,8 +2607,15 @@ exports.TransactionTriggerProcessType = void 0;
|
|
|
2607
2607
|
TransactionTriggerProcessType["CAMPAIGN_BUSINESS_CLAIM"] = "CAMPAIGN_BUSINESS_CLAIM";
|
|
2608
2608
|
TransactionTriggerProcessType["REDEMPTION_SPEND"] = "REDEMPTION_SPEND";
|
|
2609
2609
|
TransactionTriggerProcessType["REDEMPTION_RECEIVE"] = "REDEMPTION_RECEIVE";
|
|
2610
|
+
/**
|
|
2611
|
+
* Price token transfer during redemption (ERC20/ERC721/ERC1155 payment).
|
|
2612
|
+
* Does NOT trigger reward minting — the redemption lifecycle is driven by
|
|
2613
|
+
* REDEMPTION_SPEND (credit) or direct minting for free redemptions.
|
|
2614
|
+
*/
|
|
2615
|
+
TransactionTriggerProcessType["REDEMPTION_PRICE_TOKEN_TRANSFER"] = "REDEMPTION_PRICE_TOKEN_TRANSFER";
|
|
2610
2616
|
TransactionTriggerProcessType["MIGRATION"] = "MIGRATION";
|
|
2611
2617
|
TransactionTriggerProcessType["ADMIN_TRIGGERED"] = "ADMIN_TRIGGERED";
|
|
2618
|
+
TransactionTriggerProcessType["BUSINESS_TRIGGERED"] = "BUSINESS_TRIGGERED";
|
|
2612
2619
|
})(exports.TransactionTriggerProcessType || (exports.TransactionTriggerProcessType = {}));
|
|
2613
2620
|
|
|
2614
2621
|
exports.ApiKeyType = void 0;
|
|
@@ -2620,6 +2627,7 @@ exports.ApiKeyType = void 0;
|
|
|
2620
2627
|
ApiKeyType["USER_JWT_REFRESH_TOKEN"] = "USER_JWT_REFRESH_TOKEN";
|
|
2621
2628
|
ApiKeyType["BUSINESS_JWT_ACCESS_TOKEN"] = "BUSINESS_JWT_ACCESS_TOKEN";
|
|
2622
2629
|
ApiKeyType["BUSINESS_JWT_REFRESH_TOKEN"] = "BUSINESS_JWT_REFRESH_TOKEN";
|
|
2630
|
+
ApiKeyType["BUSINESS_SYSTEM_JWT"] = "BUSINESS_SYSTEM_JWT";
|
|
2623
2631
|
ApiKeyType["BLOCKCHAIN_WRITER_JWT"] = "BLOCKCHAIN_WRITER_JWT";
|
|
2624
2632
|
ApiKeyType["BLOCKCHAIN_READER_JWT"] = "BLOCKCHAIN_READER_JWT";
|
|
2625
2633
|
ApiKeyType["TRANSACTION_JWT_ACCESS_TOKEN"] = "TRANSACTION_JWT_ACCESS_TOKEN";
|
|
@@ -2701,6 +2709,11 @@ exports.Web3ContractTypes = void 0;
|
|
|
2701
2709
|
Web3ContractTypes["COUNTERFACTUAL_WALLET_PROXY"] = "COUNTERFACTUAL_WALLET_PROXY";
|
|
2702
2710
|
// Token Contracts
|
|
2703
2711
|
Web3ContractTypes["TOKEN_CONTRACT"] = "TOKEN_CONTRACT";
|
|
2712
|
+
// Token Deployment Infrastructure
|
|
2713
|
+
Web3ContractTypes["TOKEN_IMPLEMENTATION"] = "TOKEN_IMPLEMENTATION";
|
|
2714
|
+
Web3ContractTypes["TOKEN_PROXY_FACTORY"] = "TOKEN_PROXY_FACTORY";
|
|
2715
|
+
// Notification Infrastructure
|
|
2716
|
+
Web3ContractTypes["NOTIFICATION_HUB"] = "NOTIFICATION_HUB";
|
|
2704
2717
|
})(exports.Web3ContractTypes || (exports.Web3ContractTypes = {}));
|
|
2705
2718
|
|
|
2706
2719
|
/**
|
|
@@ -2737,6 +2750,8 @@ exports.Web3MasterWalletTypes = void 0;
|
|
|
2737
2750
|
Web3MasterWalletTypes["PROXY_FACTORY_OWNER"] = "PROXY_FACTORY_OWNER";
|
|
2738
2751
|
Web3MasterWalletTypes["RELAYER"] = "RELAYER";
|
|
2739
2752
|
Web3MasterWalletTypes["TOKEN_OWNER"] = "TOKEN_OWNER";
|
|
2753
|
+
Web3MasterWalletTypes["TENANT_WALLET"] = "TENANT_WALLET";
|
|
2754
|
+
Web3MasterWalletTypes["NOTIFIER"] = "NOTIFIER";
|
|
2740
2755
|
})(exports.Web3MasterWalletTypes || (exports.Web3MasterWalletTypes = {}));
|
|
2741
2756
|
|
|
2742
2757
|
/**
|
|
@@ -2751,6 +2766,7 @@ exports.SigningAccountProvider = void 0;
|
|
|
2751
2766
|
WALLETCONNECT = 'walletconnect',
|
|
2752
2767
|
COINBASE_WALLET = 'coinbase_wallet', */
|
|
2753
2768
|
SigningAccountProvider["DFNS"] = "DFNS";
|
|
2769
|
+
SigningAccountProvider["TURNKEY"] = "TURNKEY";
|
|
2754
2770
|
// CUSTODIAL = 'custodial'
|
|
2755
2771
|
SigningAccountProvider["PERS_INTERNAL"] = "PERS_INTERNAL"; // Internal PERS managed signing accounts
|
|
2756
2772
|
})(exports.SigningAccountProvider || (exports.SigningAccountProvider = {}));
|
|
@@ -3156,6 +3172,7 @@ exports.WebhookAuthType = void 0;
|
|
|
3156
3172
|
WebhookAuthType["NONE"] = "NONE";
|
|
3157
3173
|
WebhookAuthType["BEARER"] = "BEARER";
|
|
3158
3174
|
WebhookAuthType["JWT"] = "JWT";
|
|
3175
|
+
WebhookAuthType["HMAC"] = "HMAC";
|
|
3159
3176
|
})(exports.WebhookAuthType || (exports.WebhookAuthType = {}));
|
|
3160
3177
|
/**
|
|
3161
3178
|
* Sources that can invoke a webhook
|
|
@@ -3213,6 +3230,47 @@ const OttAuthMethod = {
|
|
|
3213
3230
|
EMAIL_CODE: 'email-code',
|
|
3214
3231
|
};
|
|
3215
3232
|
|
|
3233
|
+
/**
|
|
3234
|
+
* Describes the economic mechanism used to issue a reward token.
|
|
3235
|
+
* Persisted on the transaction record for audit, analytics, and AML reporting.
|
|
3236
|
+
*
|
|
3237
|
+
* IMPORTANT: This records economic intent/attribution, not the on-chain executor.
|
|
3238
|
+
* Per-business on-chain execution is a Phase 2 milestone:
|
|
3239
|
+
* - BUSINESS_BALANCE_TRANSFER: requires transferFrom + allowance or AA smart-wallet as msg.sender
|
|
3240
|
+
* - BUSINESS_OWNED_MINT: requires per-business MINTER_ROLE (granted eagerly at metadata ownership assignment)
|
|
3241
|
+
* In the interim, business custodial smart wallets are routed through EIP-712 when CUSTODIAL wallet
|
|
3242
|
+
* type is detected (Step 7 of server-transaction.workflow.ts).
|
|
3243
|
+
*
|
|
3244
|
+
* Decision matrix (resolved at claim time by the reward orchestrators):
|
|
3245
|
+
* ERC20, canMintToken=true → TENANT_DELEGATED_MINT (tenant master wallet mints)
|
|
3246
|
+
* ERC20, canMintToken=false → BUSINESS_BALANCE_TRANSFER (business smart wallet transfers)
|
|
3247
|
+
* NFT, metadata.ownerBusinessId matches → BUSINESS_OWNED_MINT (MINTER_ROLE granted at ownership assignment)
|
|
3248
|
+
* NFT, no business ownership match → TENANT_DELEGATED_MINT
|
|
3249
|
+
* Non-reward transactions (transfer, burn, legacy) → null (not set)
|
|
3250
|
+
*/
|
|
3251
|
+
exports.TokenIssuanceMode = void 0;
|
|
3252
|
+
(function (TokenIssuanceMode) {
|
|
3253
|
+
/**
|
|
3254
|
+
* Tenant master wallet mints new supply (default path).
|
|
3255
|
+
* Applies to all NFTs without business-ownership attribution and ERC20 with canMintToken=true.
|
|
3256
|
+
* On-chain: tenant master wallet calls mint(); cost absorbed by platform.
|
|
3257
|
+
*/
|
|
3258
|
+
TokenIssuanceMode["TENANT_DELEGATED_MINT"] = "TENANT_DELEGATED_MINT";
|
|
3259
|
+
/**
|
|
3260
|
+
* Business owns the NFT template (metadata.ownerBusinessId matches engaging business).
|
|
3261
|
+
* MINTER_ROLE is eagerly granted to the business custodial wallet at metadata ownership assignment.
|
|
3262
|
+
* On-chain: business custodial smart wallet is msg.sender via EIP-712 path (Step 7).
|
|
3263
|
+
*/
|
|
3264
|
+
TokenIssuanceMode["BUSINESS_OWNED_MINT"] = "BUSINESS_OWNED_MINT";
|
|
3265
|
+
/**
|
|
3266
|
+
* Business transfers from its own on-chain ERC20 balance. No new supply created.
|
|
3267
|
+
* Cap = business wallet balance. Triggered when canMintToken=false on the engaging business.
|
|
3268
|
+
* On-chain: business custodial smart wallet is msg.sender via EIP-712 path (Step 7).
|
|
3269
|
+
* Balance pre-check is enforced by validateTokenTransfer (Step 5) before submission.
|
|
3270
|
+
*/
|
|
3271
|
+
TokenIssuanceMode["BUSINESS_BALANCE_TRANSFER"] = "BUSINESS_BALANCE_TRANSFER";
|
|
3272
|
+
})(exports.TokenIssuanceMode || (exports.TokenIssuanceMode = {}));
|
|
3273
|
+
|
|
3216
3274
|
/**
|
|
3217
3275
|
* Base array of all string-valued user model property keys.
|
|
3218
3276
|
* Single source of truth for user field names across the codebase.
|
|
@@ -3529,7 +3587,7 @@ const validateFieldValue = (value, rules, fieldType) => {
|
|
|
3529
3587
|
break;
|
|
3530
3588
|
case 'phone':
|
|
3531
3589
|
// Basic phone validation - allows digits, spaces, dashes, parentheses, plus
|
|
3532
|
-
const phoneRegex = /^[\d\s
|
|
3590
|
+
const phoneRegex = /^[\d\s\-()+]+$/;
|
|
3533
3591
|
if (!phoneRegex.test(stringValue)) {
|
|
3534
3592
|
return { field: '', message: 'Invalid phone number format', code: 'phone' };
|
|
3535
3593
|
}
|
|
@@ -3661,6 +3719,399 @@ const validateAllFields = (data, definitions) => {
|
|
|
3661
3719
|
return errors;
|
|
3662
3720
|
};
|
|
3663
3721
|
|
|
3722
|
+
/**
|
|
3723
|
+
* User status hierarchy resolution (pure, framework-agnostic).
|
|
3724
|
+
*
|
|
3725
|
+
* Centralizes the rule for "does a user's earned status satisfy a minimum
|
|
3726
|
+
* requirement?" so backend and frontend evaluate it identically.
|
|
3727
|
+
*
|
|
3728
|
+
* Hierarchy semantics:
|
|
3729
|
+
* - Exact match always qualifies.
|
|
3730
|
+
* - Otherwise a user qualifies if they hold a status in the SAME hierarchy with
|
|
3731
|
+
* an `order` greater than or equal to the required status `order`.
|
|
3732
|
+
* - Hierarchies (tracks) are separated by tags: statuses with no tags share the
|
|
3733
|
+
* global hierarchy; tagged statuses share a track only when their tags overlap.
|
|
3734
|
+
* - A required status without an `order` requires an exact match (backward compatible).
|
|
3735
|
+
*/
|
|
3736
|
+
/**
|
|
3737
|
+
* Returns true if any two tag arrays share at least one tag (case-insensitive).
|
|
3738
|
+
* Statuses that overlap on tags belong to the same track.
|
|
3739
|
+
*/
|
|
3740
|
+
function tagsOverlap(tags1, tags2) {
|
|
3741
|
+
if (!tags1.length || !tags2.length)
|
|
3742
|
+
return false;
|
|
3743
|
+
const set1 = new Set(tags1.map(t => t.toLowerCase()));
|
|
3744
|
+
return tags2.some(tag => set1.has(tag.toLowerCase()));
|
|
3745
|
+
}
|
|
3746
|
+
/**
|
|
3747
|
+
* Determines whether a user meets a minimum status requirement, honoring the
|
|
3748
|
+
* tag-scoped status hierarchy.
|
|
3749
|
+
*
|
|
3750
|
+
* @param requiredStatusId - The minimum status type ID required.
|
|
3751
|
+
* @param userStatusIds - Status type IDs the user has earned.
|
|
3752
|
+
* @param allStatusTypes - All status types (must include the required and user statuses).
|
|
3753
|
+
* @returns True if the user satisfies the requirement (exact match or higher in same track).
|
|
3754
|
+
*/
|
|
3755
|
+
function meetsMinimumStatusRequirement(requiredStatusId, userStatusIds, allStatusTypes) {
|
|
3756
|
+
// Quick check: exact match
|
|
3757
|
+
if (userStatusIds.includes(requiredStatusId)) {
|
|
3758
|
+
return true;
|
|
3759
|
+
}
|
|
3760
|
+
const statusMap = new Map(allStatusTypes.map(s => [s.id, s]));
|
|
3761
|
+
const requiredStatus = statusMap.get(requiredStatusId);
|
|
3762
|
+
if (!requiredStatus) {
|
|
3763
|
+
return false; // Required status doesn't exist
|
|
3764
|
+
}
|
|
3765
|
+
const requiredOrder = requiredStatus.order;
|
|
3766
|
+
if (requiredOrder === null || requiredOrder === undefined) {
|
|
3767
|
+
// No order on required status = require exact match only (backward compatible)
|
|
3768
|
+
return false;
|
|
3769
|
+
}
|
|
3770
|
+
const requiredTags = requiredStatus.tags || [];
|
|
3771
|
+
// Check if user has any status with equal or higher order in the same hierarchy
|
|
3772
|
+
for (const userStatusId of userStatusIds) {
|
|
3773
|
+
const userStatus = statusMap.get(userStatusId);
|
|
3774
|
+
if (!userStatus)
|
|
3775
|
+
continue;
|
|
3776
|
+
const userOrder = userStatus.order;
|
|
3777
|
+
if (userOrder === null || userOrder === undefined)
|
|
3778
|
+
continue; // No order
|
|
3779
|
+
const userTags = userStatus.tags || [];
|
|
3780
|
+
// Same hierarchy when: both untagged (global), OR they share a track tag.
|
|
3781
|
+
const sameHierarchy = (requiredTags.length === 0 && userTags.length === 0) ||
|
|
3782
|
+
tagsOverlap(requiredTags, userTags);
|
|
3783
|
+
if (!sameHierarchy)
|
|
3784
|
+
continue; // Different hierarchy
|
|
3785
|
+
if (userOrder >= requiredOrder) {
|
|
3786
|
+
return true;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
return false;
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
/**
|
|
3793
|
+
* Canonical list of redemption eligibility gates.
|
|
3794
|
+
*
|
|
3795
|
+
* ⚠️ SYNC CONTRACT: These gates must mirror the validation steps in
|
|
3796
|
+
* `RedemptionRedeemWorkflow` (backend). If you add or remove a gate here,
|
|
3797
|
+
* update the workflow too — and vice versa.
|
|
3798
|
+
*
|
|
3799
|
+
* Use this constant in tests to assert that `evaluateRedemptionEligibility`
|
|
3800
|
+
* returns exactly these keys (no more, no less):
|
|
3801
|
+
*
|
|
3802
|
+
* expect(Object.keys(result.checks)).toEqual([...REDEMPTION_ELIGIBILITY_GATES]);
|
|
3803
|
+
*/
|
|
3804
|
+
const REDEMPTION_ELIGIBILITY_GATES = [
|
|
3805
|
+
'userActive',
|
|
3806
|
+
'requiredFields',
|
|
3807
|
+
'booking',
|
|
3808
|
+
'userStatus',
|
|
3809
|
+
'maxPerUser',
|
|
3810
|
+
];
|
|
3811
|
+
function makeCheck(passed, code, message, detail) {
|
|
3812
|
+
return passed
|
|
3813
|
+
? { passed: true, reason: null }
|
|
3814
|
+
: { passed: false, reason: { code, message }, ...(detail ? { detail } : {}) };
|
|
3815
|
+
}
|
|
3816
|
+
/**
|
|
3817
|
+
* Derives an aggregate redemption eligibility verdict from the user, redemption and status list.
|
|
3818
|
+
*
|
|
3819
|
+
* Pure and side-effect free: pass the DTOs directly from your API responses; the function
|
|
3820
|
+
* computes all per-gate verdicts (including status hierarchy and maxPerUser math) internally.
|
|
3821
|
+
*
|
|
3822
|
+
* Requires `?include=requiredUserInfo,booking` on the redemption fetch and
|
|
3823
|
+
* `?include=status` on the user fetch for full accuracy.
|
|
3824
|
+
*/
|
|
3825
|
+
function evaluateRedemptionEligibility(input) {
|
|
3826
|
+
const { user, redemption, allStatusTypes, userRedeemCount } = input;
|
|
3827
|
+
const userStatusTypeIds = (user.included?.statusTypes ?? []).map(s => s.id).filter((id) => id !== undefined);
|
|
3828
|
+
const minUserStatusTypeId = redemption.minUserStatusType?.id ?? null;
|
|
3829
|
+
const requiredUserInfo = redemption.included?.requiredUserInfo;
|
|
3830
|
+
const booking = redemption.included?.booking;
|
|
3831
|
+
// userStatus — delegate to shared pure hierarchy check
|
|
3832
|
+
const userStatusPassed = !minUserStatusTypeId ||
|
|
3833
|
+
meetsMinimumStatusRequirement(minUserStatusTypeId, userStatusTypeIds, allStatusTypes);
|
|
3834
|
+
// maxPerUser — pure numeric comparison
|
|
3835
|
+
const maxPerUserPassed = !redemption.maxPerUser || userRedeemCount < redemption.maxPerUser;
|
|
3836
|
+
// requiredFields — use isValid from the include; derive missing keys from field definitions
|
|
3837
|
+
const requiredFieldsPassed = !requiredUserInfo || requiredUserInfo.isValid;
|
|
3838
|
+
const missingFieldKeys = requiredUserInfo?.fieldDefinitions
|
|
3839
|
+
?.filter(f => f.isMissing)
|
|
3840
|
+
.map(f => f.key ?? f.id ?? '')
|
|
3841
|
+
.filter(Boolean) ?? [];
|
|
3842
|
+
const checks = {
|
|
3843
|
+
userActive: makeCheck(user.isActive !== false, 'USER_INACTIVE', 'User account is inactive'),
|
|
3844
|
+
requiredFields: makeCheck(requiredFieldsPassed, 'MISSING_REQUIRED_FIELDS', 'User is missing one or more required fields', requiredFieldsPassed ? undefined : { missingFieldKeys }),
|
|
3845
|
+
booking: makeCheck(!booking || booking.isValid, 'BOOKING_REQUIRED', 'User does not have a valid booking meeting the requirements'),
|
|
3846
|
+
userStatus: makeCheck(userStatusPassed, 'INSUFFICIENT_USER_STATUS', 'User does not meet the minimum status requirement', userStatusPassed || !minUserStatusTypeId ? undefined : {
|
|
3847
|
+
minRequiredStatusTypeId: minUserStatusTypeId,
|
|
3848
|
+
userStatusTypeIds,
|
|
3849
|
+
}),
|
|
3850
|
+
maxPerUser: makeCheck(maxPerUserPassed, 'MAX_PER_USER_REACHED', 'User has reached the maximum number of redeems allowed', maxPerUserPassed || !redemption.maxPerUser ? undefined : {
|
|
3851
|
+
maxPerUser: redemption.maxPerUser,
|
|
3852
|
+
userRedeemCount,
|
|
3853
|
+
}),
|
|
3854
|
+
};
|
|
3855
|
+
return {
|
|
3856
|
+
isEligible: Object.values(checks).every(c => c.passed),
|
|
3857
|
+
checks,
|
|
3858
|
+
};
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3861
|
+
/**
|
|
3862
|
+
* Campaign trigger-source access rule (pure, framework-agnostic).
|
|
3863
|
+
*
|
|
3864
|
+
* Single source of truth for "may this business assign/remove/create a trigger
|
|
3865
|
+
* source against this campaign?", shared by CampaignService (assign/remove) and
|
|
3866
|
+
* TriggerSourceService (optional campaignId at creation) — two different NestJS
|
|
3867
|
+
* modules that each fetch the campaign via their own query, but must agree on
|
|
3868
|
+
* the same rule instead of maintaining separate copies.
|
|
3869
|
+
*
|
|
3870
|
+
* Rules:
|
|
3871
|
+
* - No requestingBusinessId (tenant admin) — always allowed.
|
|
3872
|
+
* - Tenant-level campaign (no ownerBusinessId) — allowed for any business; the
|
|
3873
|
+
* specific trigger source's own businessId ownership is checked separately.
|
|
3874
|
+
* - Business-owned campaign — only the owning business is allowed.
|
|
3875
|
+
*/
|
|
3876
|
+
function canManageCampaignTriggerSources(campaign, requestingBusinessId) {
|
|
3877
|
+
if (!requestingBusinessId)
|
|
3878
|
+
return true;
|
|
3879
|
+
return !campaign.ownerBusinessId || campaign.ownerBusinessId === requestingBusinessId;
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
/**
|
|
3883
|
+
* Computed lifecycle status of a campaign/redemption (never stored).
|
|
3884
|
+
* Precedence mirrors assertAvailable: inactive, then ended, then scheduled, else active.
|
|
3885
|
+
*/
|
|
3886
|
+
exports.AvailabilityStatus = void 0;
|
|
3887
|
+
(function (AvailabilityStatus) {
|
|
3888
|
+
AvailabilityStatus["INACTIVE"] = "inactive";
|
|
3889
|
+
AvailabilityStatus["SCHEDULED"] = "scheduled";
|
|
3890
|
+
AvailabilityStatus["ACTIVE"] = "active";
|
|
3891
|
+
AvailabilityStatus["ENDED"] = "ended";
|
|
3892
|
+
})(exports.AvailabilityStatus || (exports.AvailabilityStatus = {}));
|
|
3893
|
+
|
|
3894
|
+
/** Precedence: inactive, ended, scheduled, active (same order as the SQL status filter). */
|
|
3895
|
+
function getAvailabilityStatus(entity, now = new Date()) {
|
|
3896
|
+
if (!entity.isActive)
|
|
3897
|
+
return exports.AvailabilityStatus.INACTIVE;
|
|
3898
|
+
if (entity.endDate && new Date(entity.endDate) < now)
|
|
3899
|
+
return exports.AvailabilityStatus.ENDED;
|
|
3900
|
+
if (entity.startDate && new Date(entity.startDate) > now)
|
|
3901
|
+
return exports.AvailabilityStatus.SCHEDULED;
|
|
3902
|
+
return exports.AvailabilityStatus.ACTIVE;
|
|
3903
|
+
}
|
|
3904
|
+
function assertAvailable(entity, errors, now = new Date()) {
|
|
3905
|
+
switch (getAvailabilityStatus(entity, now)) {
|
|
3906
|
+
case exports.AvailabilityStatus.INACTIVE: throw errors.notActive();
|
|
3907
|
+
case exports.AvailabilityStatus.ENDED: throw errors.ended(new Date(entity.endDate));
|
|
3908
|
+
case exports.AvailabilityStatus.SCHEDULED: throw errors.notStarted(new Date(entity.startDate));
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
/** Opt-in request header; `Accept-Language` is deliberately not used (browsers send it implicitly). */
|
|
3913
|
+
const LOCALE_HEADER = 'x-locale';
|
|
3914
|
+
const LOCALE_MAX_LENGTH = 35;
|
|
3915
|
+
const TRANSLATIONS_MAX_LOCALES = 30;
|
|
3916
|
+
const TRANSLATION_VALUE_MAX_LENGTH = 4000;
|
|
3917
|
+
const TRANSLATION_FIELD_MAX_LENGTH = 100;
|
|
3918
|
+
const TRANSLATIONS_MAX_SIZE = 32 * 1024;
|
|
3919
|
+
const LOCALE_PATTERN = /^[a-z]{2,3}(-[a-z0-9]{2,8}){0,3}$/;
|
|
3920
|
+
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
3921
|
+
/** Returns a lowercase BCP 47 tag (`en_US` -> `en-us`), or null when absent or malformed. */
|
|
3922
|
+
function normalizeLocale(raw) {
|
|
3923
|
+
if (typeof raw !== 'string')
|
|
3924
|
+
return null;
|
|
3925
|
+
const value = raw.trim().replace(/_/g, '-').toLowerCase();
|
|
3926
|
+
if (value.length === 0 || value.length > LOCALE_MAX_LENGTH || !LOCALE_PATTERN.test(value))
|
|
3927
|
+
return null;
|
|
3928
|
+
return value;
|
|
3929
|
+
}
|
|
3930
|
+
/**
|
|
3931
|
+
* Per-field lookup: exact tag, then progressively shorter tags (`ar-sa` -> `ar`), then `base`.
|
|
3932
|
+
* Empty or missing translated values fall through.
|
|
3933
|
+
*/
|
|
3934
|
+
function resolveTranslation(translations, locale, field, base) {
|
|
3935
|
+
if (!translations || !locale)
|
|
3936
|
+
return base;
|
|
3937
|
+
const parts = locale.split('-');
|
|
3938
|
+
for (let length = parts.length; length > 0; length--) {
|
|
3939
|
+
const key = parts.slice(0, length).join('-');
|
|
3940
|
+
if (!hasOwn(translations, key))
|
|
3941
|
+
continue;
|
|
3942
|
+
const entry = translations[key];
|
|
3943
|
+
if (!entry || !hasOwn(entry, field))
|
|
3944
|
+
continue;
|
|
3945
|
+
const value = entry[field];
|
|
3946
|
+
if (typeof value === 'string' && value.length > 0)
|
|
3947
|
+
return value;
|
|
3948
|
+
}
|
|
3949
|
+
return base;
|
|
3950
|
+
}
|
|
3951
|
+
/** Size/count limits for a whole stored map. Returns an error message, or null when valid. */
|
|
3952
|
+
function validateTranslationsLimits(map) {
|
|
3953
|
+
if (!map)
|
|
3954
|
+
return null;
|
|
3955
|
+
if (Object.keys(map).length > TRANSLATIONS_MAX_LOCALES) {
|
|
3956
|
+
return `at most ${TRANSLATIONS_MAX_LOCALES} locales are allowed`;
|
|
3957
|
+
}
|
|
3958
|
+
if (JSON.stringify(map).length > TRANSLATIONS_MAX_SIZE) {
|
|
3959
|
+
return `translations exceed the ${TRANSLATIONS_MAX_SIZE} character limit`;
|
|
3960
|
+
}
|
|
3961
|
+
return null;
|
|
3962
|
+
}
|
|
3963
|
+
/** An allowed entry ending in `*` is a prefix wildcard (e.g. `option.*` for keyed nested texts). */
|
|
3964
|
+
function isAllowedField(field, allowedFields) {
|
|
3965
|
+
if (field.length > TRANSLATION_FIELD_MAX_LENGTH)
|
|
3966
|
+
return false;
|
|
3967
|
+
return allowedFields.some(allowed => allowed === field || (allowed.endsWith('*') && field.length >= allowed.length && field.startsWith(allowed.slice(0, -1))));
|
|
3968
|
+
}
|
|
3969
|
+
/** Structural validation of a write payload. Returns an error message, or null when valid. */
|
|
3970
|
+
function validateTranslationsPatch(input, allowedFields) {
|
|
3971
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
3972
|
+
return 'translations must be an object keyed by locale';
|
|
3973
|
+
}
|
|
3974
|
+
const entries = Object.entries(input);
|
|
3975
|
+
if (entries.length > TRANSLATIONS_MAX_LOCALES) {
|
|
3976
|
+
return `at most ${TRANSLATIONS_MAX_LOCALES} locales are allowed`;
|
|
3977
|
+
}
|
|
3978
|
+
for (const [rawLocale, entry] of entries) {
|
|
3979
|
+
if (normalizeLocale(rawLocale) === null)
|
|
3980
|
+
return `invalid locale "${rawLocale.slice(0, LOCALE_MAX_LENGTH)}"`;
|
|
3981
|
+
if (entry === null)
|
|
3982
|
+
continue;
|
|
3983
|
+
if (typeof entry !== 'object' || Array.isArray(entry)) {
|
|
3984
|
+
return `translations for "${rawLocale}" must be an object or null`;
|
|
3985
|
+
}
|
|
3986
|
+
for (const [field, value] of Object.entries(entry)) {
|
|
3987
|
+
if (!isAllowedField(field, allowedFields))
|
|
3988
|
+
return `field "${field.slice(0, 50)}" is not translatable`;
|
|
3989
|
+
if (value === null)
|
|
3990
|
+
continue;
|
|
3991
|
+
if (typeof value !== 'string')
|
|
3992
|
+
return `"${rawLocale}.${field}" must be a string or null`;
|
|
3993
|
+
if (value.length > TRANSLATION_VALUE_MAX_LENGTH) {
|
|
3994
|
+
return `"${rawLocale}.${field}" exceeds ${TRANSLATION_VALUE_MAX_LENGTH} characters`;
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
return validateTranslationsLimits(normalizePatchKeys(input));
|
|
3999
|
+
}
|
|
4000
|
+
function normalizePatchKeys(patch) {
|
|
4001
|
+
return applyPatch({}, patch) ?? {};
|
|
4002
|
+
}
|
|
4003
|
+
function applyPatch(base, patch) {
|
|
4004
|
+
const result = { ...base };
|
|
4005
|
+
for (const [rawLocale, entry] of Object.entries(patch)) {
|
|
4006
|
+
const locale = normalizeLocale(rawLocale);
|
|
4007
|
+
if (locale === null)
|
|
4008
|
+
continue;
|
|
4009
|
+
if (entry === null) {
|
|
4010
|
+
delete result[locale];
|
|
4011
|
+
continue;
|
|
4012
|
+
}
|
|
4013
|
+
const fields = {};
|
|
4014
|
+
for (const [field, value] of Object.entries(entry)) {
|
|
4015
|
+
if (typeof value === 'string' && value.length > 0)
|
|
4016
|
+
fields[field] = value;
|
|
4017
|
+
}
|
|
4018
|
+
if (Object.keys(fields).length > 0)
|
|
4019
|
+
result[locale] = fields;
|
|
4020
|
+
else
|
|
4021
|
+
delete result[locale];
|
|
4022
|
+
}
|
|
4023
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
4024
|
+
}
|
|
4025
|
+
/** Applies a validated patch to the stored map; returns null when nothing remains. */
|
|
4026
|
+
function mergeTranslations(existing, patch) {
|
|
4027
|
+
return applyPatch(existing ?? {}, patch);
|
|
4028
|
+
}
|
|
4029
|
+
/** Client-side clean-up of an edited map: trims values, drops blank ones and locales left without text, normalizes locale keys. */
|
|
4030
|
+
function cleanTranslations(map) {
|
|
4031
|
+
const result = {};
|
|
4032
|
+
for (const [rawLocale, fields] of Object.entries(map ?? {})) {
|
|
4033
|
+
const locale = normalizeLocale(rawLocale);
|
|
4034
|
+
if (locale === null)
|
|
4035
|
+
continue;
|
|
4036
|
+
const kept = {};
|
|
4037
|
+
for (const [field, value] of Object.entries(fields ?? {})) {
|
|
4038
|
+
const text = typeof value === 'string' ? value.trim() : '';
|
|
4039
|
+
if (text.length > 0)
|
|
4040
|
+
kept[field] = text;
|
|
4041
|
+
}
|
|
4042
|
+
if (Object.keys(kept).length > 0)
|
|
4043
|
+
result[locale] = kept;
|
|
4044
|
+
}
|
|
4045
|
+
return result;
|
|
4046
|
+
}
|
|
4047
|
+
const sameFields = (a, b) => {
|
|
4048
|
+
const keys = Object.keys(a);
|
|
4049
|
+
return keys.length === Object.keys(b).length && keys.every(key => a[key] === b[key]);
|
|
4050
|
+
};
|
|
4051
|
+
/**
|
|
4052
|
+
* Write payload for editing: a changed locale replaces that locale, a removed one is `null`.
|
|
4053
|
+
* Returns `undefined` when nothing changed, so the field can be omitted and stored translations stay untouched.
|
|
4054
|
+
* `mergeTranslations(original, patch)` yields `cleanTranslations(current)`.
|
|
4055
|
+
*/
|
|
4056
|
+
function buildTranslationsPatch(original, current) {
|
|
4057
|
+
const before = cleanTranslations(original);
|
|
4058
|
+
const after = cleanTranslations(current);
|
|
4059
|
+
const patch = {};
|
|
4060
|
+
for (const [locale, fields] of Object.entries(after)) {
|
|
4061
|
+
if (!before[locale] || !sameFields(before[locale], fields))
|
|
4062
|
+
patch[locale] = fields;
|
|
4063
|
+
}
|
|
4064
|
+
for (const locale of Object.keys(before)) {
|
|
4065
|
+
if (!after[locale])
|
|
4066
|
+
patch[locale] = null;
|
|
4067
|
+
}
|
|
4068
|
+
return Object.keys(patch).length > 0 ? patch : undefined;
|
|
4069
|
+
}
|
|
4070
|
+
/** Resolves the given translatable fields of a model for `locale`; base text when none applies. */
|
|
4071
|
+
function localizeFields(model, locale, fields) {
|
|
4072
|
+
const result = {};
|
|
4073
|
+
for (const field of fields) {
|
|
4074
|
+
result[field] = (locale
|
|
4075
|
+
? resolveTranslation(model.translations, locale, field, (model[field] ?? null))
|
|
4076
|
+
: model[field]);
|
|
4077
|
+
}
|
|
4078
|
+
return result;
|
|
4079
|
+
}
|
|
4080
|
+
/** Restricts a translations map to `fields`; locales left empty are dropped. Null when nothing remains. */
|
|
4081
|
+
function pickTranslationFields(map, fields) {
|
|
4082
|
+
if (!map)
|
|
4083
|
+
return null;
|
|
4084
|
+
const result = {};
|
|
4085
|
+
for (const [locale, entry] of Object.entries(map)) {
|
|
4086
|
+
const picked = {};
|
|
4087
|
+
for (const field of fields) {
|
|
4088
|
+
if (hasOwn(entry, field) && typeof entry[field] === 'string')
|
|
4089
|
+
picked[field] = entry[field];
|
|
4090
|
+
}
|
|
4091
|
+
if (Object.keys(picked).length > 0)
|
|
4092
|
+
result[locale] = picked;
|
|
4093
|
+
}
|
|
4094
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
4095
|
+
}
|
|
4096
|
+
/**
|
|
4097
|
+
* Translations for a row that inherits unset fields from a template: each field's translations
|
|
4098
|
+
* come from the same source as its base value (the row when it sets the field, otherwise the template).
|
|
4099
|
+
*/
|
|
4100
|
+
function inheritTranslations(row, template, fields) {
|
|
4101
|
+
const fromRow = fields.filter(field => !template || row[field] != null);
|
|
4102
|
+
const fromTemplate = fields.filter(field => !!template && row[field] == null);
|
|
4103
|
+
const merged = {};
|
|
4104
|
+
for (const [source, picked] of [
|
|
4105
|
+
[row.translations, fromRow],
|
|
4106
|
+
[template?.translations, fromTemplate],
|
|
4107
|
+
]) {
|
|
4108
|
+
for (const [locale, entry] of Object.entries(pickTranslationFields(source, picked) ?? {})) {
|
|
4109
|
+
merged[locale] = { ...merged[locale], ...entry };
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
return Object.keys(merged).length > 0 ? merged : null;
|
|
4113
|
+
}
|
|
4114
|
+
|
|
3664
4115
|
/**
|
|
3665
4116
|
* Built-in user profile fields that can be required for redemptions.
|
|
3666
4117
|
* These are a subset of UserModelField - excludes 'id' and 'identifierEmail'
|
|
@@ -3835,6 +4286,7 @@ const EntityTypes = {
|
|
|
3835
4286
|
FILE: 'file',
|
|
3836
4287
|
JWKS: 'jwks',
|
|
3837
4288
|
PROJECT: 'project',
|
|
4289
|
+
EVENTS: 'events',
|
|
3838
4290
|
};
|
|
3839
4291
|
/**
|
|
3840
4292
|
* Error-specific domains (extends EntityTypes)
|
|
@@ -3859,6 +4311,68 @@ const AuditEntityTypes = {
|
|
|
3859
4311
|
...EntityTypes,
|
|
3860
4312
|
};
|
|
3861
4313
|
|
|
4314
|
+
/**
|
|
4315
|
+
* Canonical external event-type strings — the `type` field consumers see in
|
|
4316
|
+
* delivered envelopes. Composed from {@link EntityTypes} so domain names stay
|
|
4317
|
+
* DRY. Add a new entry here first; the events module registry then decides
|
|
4318
|
+
* scope and whether to persist it.
|
|
4319
|
+
*
|
|
4320
|
+
* Convention: `<domain>.<verb>` in lowercase dot-notation.
|
|
4321
|
+
*/
|
|
4322
|
+
const ExternalEventType = {
|
|
4323
|
+
// Users
|
|
4324
|
+
UserCreated: `${EntityTypes.USER}.created`,
|
|
4325
|
+
UserUpdated: `${EntityTypes.USER}.updated`,
|
|
4326
|
+
// Transactions
|
|
4327
|
+
TransactionCreated: `${EntityTypes.TRANSACTION}.created`,
|
|
4328
|
+
TransactionMintSucceeded: `${EntityTypes.TRANSACTION}.mint_succeeded`,
|
|
4329
|
+
TransactionBurnSucceeded: `${EntityTypes.TRANSACTION}.burn_succeeded`,
|
|
4330
|
+
TransactionTransferSucceeded: `${EntityTypes.TRANSACTION}.transfer_succeeded`,
|
|
4331
|
+
// Campaigns
|
|
4332
|
+
CampaignCreated: `${EntityTypes.CAMPAIGN}.created`,
|
|
4333
|
+
CampaignUpdated: `${EntityTypes.CAMPAIGN}.updated`,
|
|
4334
|
+
CampaignClaimed: `${EntityTypes.CAMPAIGN}.claimed`,
|
|
4335
|
+
CampaignClaimFailed: `${EntityTypes.CAMPAIGN}.claim_failed`,
|
|
4336
|
+
// Redemptions
|
|
4337
|
+
RedemptionCreated: `${EntityTypes.REDEMPTION}.created`,
|
|
4338
|
+
RedemptionUpdated: `${EntityTypes.REDEMPTION}.updated`,
|
|
4339
|
+
RedemptionRedeemed: `${EntityTypes.REDEMPTION}.redeemed`,
|
|
4340
|
+
RedemptionRedeemFailed: `${EntityTypes.REDEMPTION}.redeem_failed`,
|
|
4341
|
+
// Businesses
|
|
4342
|
+
BusinessCreated: `${EntityTypes.BUSINESS}.created`,
|
|
4343
|
+
BusinessUpdated: `${EntityTypes.BUSINESS}.updated`,
|
|
4344
|
+
BusinessApproved: `${EntityTypes.BUSINESS}.approved`,
|
|
4345
|
+
BusinessRejected: `${EntityTypes.BUSINESS}.rejected`,
|
|
4346
|
+
// Campaigns (approval)
|
|
4347
|
+
CampaignApproved: `${EntityTypes.CAMPAIGN}.approved`,
|
|
4348
|
+
CampaignRejected: `${EntityTypes.CAMPAIGN}.rejected`,
|
|
4349
|
+
// Token metadata (approval)
|
|
4350
|
+
TokenMetadataApproved: `${EntityTypes.TOKEN_METADATA}.approved`,
|
|
4351
|
+
TokenMetadataRejected: `${EntityTypes.TOKEN_METADATA}.rejected`,
|
|
4352
|
+
// Redemptions (approval)
|
|
4353
|
+
RedemptionApproved: `${EntityTypes.REDEMPTION}.approved`,
|
|
4354
|
+
RedemptionRejected: `${EntityTypes.REDEMPTION}.rejected`,
|
|
4355
|
+
// Bookings
|
|
4356
|
+
BookingCreated: `${EntityTypes.BOOKING}.created`,
|
|
4357
|
+
BookingUpdated: `${EntityTypes.BOOKING}.updated`,
|
|
4358
|
+
// Token metadata
|
|
4359
|
+
TokenMetadataCreated: `${EntityTypes.TOKEN_METADATA}.created`,
|
|
4360
|
+
TokenMetadataUpdated: `${EntityTypes.TOKEN_METADATA}.updated`,
|
|
4361
|
+
// Auth
|
|
4362
|
+
MagicLinkSent: `${EntityTypes.USER}.magic_link_sent`,
|
|
4363
|
+
};
|
|
4364
|
+
|
|
4365
|
+
/**
|
|
4366
|
+
* Approval status for entities that may require tenant admin approval before becoming active.
|
|
4367
|
+
* Controlled by tenant.approvalSettings per-entity-type flags.
|
|
4368
|
+
*/
|
|
4369
|
+
exports.ApprovalStatus = void 0;
|
|
4370
|
+
(function (ApprovalStatus) {
|
|
4371
|
+
ApprovalStatus["PENDING_APPROVAL"] = "pending_approval";
|
|
4372
|
+
ApprovalStatus["APPROVED"] = "approved";
|
|
4373
|
+
ApprovalStatus["REJECTED"] = "rejected";
|
|
4374
|
+
})(exports.ApprovalStatus || (exports.ApprovalStatus = {}));
|
|
4375
|
+
|
|
3862
4376
|
/**
|
|
3863
4377
|
* Error categories for classification and handling
|
|
3864
4378
|
*
|
|
@@ -3901,6 +4415,33 @@ exports.ErrorCategory = void 0;
|
|
|
3901
4415
|
ErrorCategory["UNKNOWN"] = "UNKNOWN";
|
|
3902
4416
|
})(exports.ErrorCategory || (exports.ErrorCategory = {}));
|
|
3903
4417
|
|
|
4418
|
+
/**
|
|
4419
|
+
* Describes the payment/redemption mechanism from the user's perspective.
|
|
4420
|
+
* Computed at read time in the mapper — never stored.
|
|
4421
|
+
*
|
|
4422
|
+
* Use this for frontend rendering decisions (card labels, CTAs, progress UI)
|
|
4423
|
+
* instead of inspecting creditTokenAmount + priceTokenUnits shape directly.
|
|
4424
|
+
*/
|
|
4425
|
+
exports.RedemptionMechanism = void 0;
|
|
4426
|
+
(function (RedemptionMechanism) {
|
|
4427
|
+
/** No payment required — redemption is free. */
|
|
4428
|
+
RedemptionMechanism["FREE"] = "FREE";
|
|
4429
|
+
/** User pays with credit tokens only (creditTokenAmount > 0, no priceTokenUnits). */
|
|
4430
|
+
RedemptionMechanism["CREDIT"] = "CREDIT";
|
|
4431
|
+
/**
|
|
4432
|
+
* User transfers ERC20 tokens as payment (priceTokenUnits with ERC20 token type).
|
|
4433
|
+
* Distinct from CREDIT: uses an on-chain token transfer, not the platform credit balance.
|
|
4434
|
+
*/
|
|
4435
|
+
RedemptionMechanism["TOKEN_PAY"] = "TOKEN_PAY";
|
|
4436
|
+
/**
|
|
4437
|
+
* User burns/transfers ERC1155 or ERC721 tokens as payment.
|
|
4438
|
+
* Typical use case: stamp card redemption (collect N stamps → redeem reward).
|
|
4439
|
+
*/
|
|
4440
|
+
RedemptionMechanism["TOKEN_BURN"] = "TOKEN_BURN";
|
|
4441
|
+
/** User pays with both creditTokenAmount AND priceTokenUnits. */
|
|
4442
|
+
RedemptionMechanism["MIXED"] = "MIXED";
|
|
4443
|
+
})(exports.RedemptionMechanism || (exports.RedemptionMechanism = {}));
|
|
4444
|
+
|
|
3904
4445
|
/**
|
|
3905
4446
|
* PERS WS-Relay Shared Types
|
|
3906
4447
|
*
|
|
@@ -3956,6 +4497,12 @@ function isErrorMessage(msg) {
|
|
|
3956
4497
|
function isPingMessage(msg) {
|
|
3957
4498
|
return msg.type === 'ping';
|
|
3958
4499
|
}
|
|
4500
|
+
/**
|
|
4501
|
+
* Type guard for notification.pending messages
|
|
4502
|
+
*/
|
|
4503
|
+
function isNotificationPendingMessage(msg) {
|
|
4504
|
+
return msg.type === 'notification.pending';
|
|
4505
|
+
}
|
|
3959
4506
|
|
|
3960
4507
|
/**
|
|
3961
4508
|
* Check if a string matches UUID format
|
|
@@ -3974,6 +4521,10 @@ const NativeTokenTypes = {
|
|
|
3974
4521
|
ERC1155: 'ERC1155',
|
|
3975
4522
|
ERC721: 'ERC721'
|
|
3976
4523
|
};
|
|
4524
|
+
/** Returns true for ERC721 and ERC1155 token types, false for ERC20. */
|
|
4525
|
+
function isNftTokenType(type) {
|
|
4526
|
+
return type === NativeTokenTypes.ERC721 || type === NativeTokenTypes.ERC1155;
|
|
4527
|
+
}
|
|
3977
4528
|
|
|
3978
4529
|
const ANALYTICS_METRIC_TYPES = ['count', 'sum', 'avg', 'min', 'max'];
|
|
3979
4530
|
const ANALYTICS_GROUP_BY_TYPES = ['month', 'week', 'day', 'year', 'quarter'];
|
|
@@ -4021,6 +4572,7 @@ const Domains = {
|
|
|
4021
4572
|
WEBHOOK: 'webhook',
|
|
4022
4573
|
EMAIL: 'email',
|
|
4023
4574
|
AUDIT: 'audit',
|
|
4575
|
+
NOTIFICATION: 'notification',
|
|
4024
4576
|
};
|
|
4025
4577
|
|
|
4026
4578
|
/**
|
|
@@ -4069,7 +4621,8 @@ const VALID_CAMPAIGN_RELATIONS = [
|
|
|
4069
4621
|
'triggerSources',
|
|
4070
4622
|
'businesses',
|
|
4071
4623
|
'claimCount',
|
|
4072
|
-
'ownerBusiness'
|
|
4624
|
+
'ownerBusiness',
|
|
4625
|
+
'translations'
|
|
4073
4626
|
];
|
|
4074
4627
|
/**
|
|
4075
4628
|
* Type guard to validate campaign include relation at runtime
|
|
@@ -4110,7 +4663,8 @@ const VALID_REDEMPTION_RELATIONS = [
|
|
|
4110
4663
|
'redeemCount',
|
|
4111
4664
|
'ownerBusiness',
|
|
4112
4665
|
'requiredUserInfo',
|
|
4113
|
-
'booking'
|
|
4666
|
+
'booking',
|
|
4667
|
+
'translations'
|
|
4114
4668
|
];
|
|
4115
4669
|
/**
|
|
4116
4670
|
* Type guard to validate redemption include relation at runtime
|
|
@@ -4156,6 +4710,13 @@ function isValidBusinessMembershipRelation(value) {
|
|
|
4156
4710
|
return VALID_BUSINESS_MEMBERSHIP_RELATIONS.includes(value);
|
|
4157
4711
|
}
|
|
4158
4712
|
|
|
4713
|
+
const VALID_BUSINESS_RELATIONS = [
|
|
4714
|
+
'translations'
|
|
4715
|
+
];
|
|
4716
|
+
function isValidBusinessRelation(value) {
|
|
4717
|
+
return VALID_BUSINESS_RELATIONS.includes(value);
|
|
4718
|
+
}
|
|
4719
|
+
|
|
4159
4720
|
/**
|
|
4160
4721
|
* Valid include relations for User endpoints.
|
|
4161
4722
|
* These relations can be requested via ?include=status,balances query parameter.
|
|
@@ -4176,7 +4737,8 @@ const VALID_TOKEN_METADATA_RELATIONS = [
|
|
|
4176
4737
|
'ownerBusiness',
|
|
4177
4738
|
'mintCount',
|
|
4178
4739
|
'burnCount',
|
|
4179
|
-
'token'
|
|
4740
|
+
'token',
|
|
4741
|
+
'translations'
|
|
4180
4742
|
];
|
|
4181
4743
|
/**
|
|
4182
4744
|
* Type guard to validate token metadata include relation at runtime
|
|
@@ -4280,7 +4842,7 @@ function computeBookingStatus(checkInDate, checkOutDate, referenceDate = new Dat
|
|
|
4280
4842
|
checkOut.setHours(0, 0, 0, 0);
|
|
4281
4843
|
// Defensive validation: checkOut must be after checkIn
|
|
4282
4844
|
// This catches invalid data from frontend or data integrity issues
|
|
4283
|
-
if (checkOut
|
|
4845
|
+
if (checkOut < checkIn) {
|
|
4284
4846
|
throw new Error(`Invalid booking dates: checkOut (${checkOut.toISOString()}) must be after checkIn (${checkIn.toISOString()})`);
|
|
4285
4847
|
}
|
|
4286
4848
|
// Checkout day itself counts as still active (guest there until end of day)
|
|
@@ -4498,28 +5060,28 @@ function parseUserAgentForPlatform(userAgent) {
|
|
|
4498
5060
|
// Detect OS and device type
|
|
4499
5061
|
if (/iPhone|iPod/i.test(ua)) {
|
|
4500
5062
|
platform.os = 'iOS';
|
|
4501
|
-
const match = ua.match(/OS (\d+)[_
|
|
5063
|
+
const match = ua.match(/OS (\d+)[_.](\d+)/);
|
|
4502
5064
|
if (match)
|
|
4503
5065
|
platform.osVersion = `${match[1]}.${match[2]}`;
|
|
4504
5066
|
platform.deviceType = 'phone';
|
|
4505
5067
|
}
|
|
4506
5068
|
else if (/iPad/i.test(ua)) {
|
|
4507
5069
|
platform.os = 'iOS';
|
|
4508
|
-
const match = ua.match(/OS (\d+)[_
|
|
5070
|
+
const match = ua.match(/OS (\d+)[_.](\d+)/);
|
|
4509
5071
|
if (match)
|
|
4510
5072
|
platform.osVersion = `${match[1]}.${match[2]}`;
|
|
4511
5073
|
platform.deviceType = 'tablet';
|
|
4512
5074
|
}
|
|
4513
5075
|
else if (/Android/i.test(ua)) {
|
|
4514
5076
|
platform.os = 'Android';
|
|
4515
|
-
const match = ua.match(/Android ([\d
|
|
5077
|
+
const match = ua.match(/Android ([\d.]+)/);
|
|
4516
5078
|
if (match)
|
|
4517
5079
|
platform.osVersion = match[1];
|
|
4518
5080
|
platform.deviceType = /Mobile/i.test(ua) ? 'phone' : 'tablet';
|
|
4519
5081
|
}
|
|
4520
5082
|
else if (/Windows NT/i.test(ua)) {
|
|
4521
5083
|
platform.os = 'Windows';
|
|
4522
|
-
const match = ua.match(/Windows NT ([\d
|
|
5084
|
+
const match = ua.match(/Windows NT ([\d.]+)/);
|
|
4523
5085
|
if (match)
|
|
4524
5086
|
platform.osVersion = match[1];
|
|
4525
5087
|
platform.deviceType = 'desktop';
|
|
@@ -4538,31 +5100,31 @@ function parseUserAgentForPlatform(userAgent) {
|
|
|
4538
5100
|
// Detect browser (web channel)
|
|
4539
5101
|
if (/Edg\//i.test(ua)) {
|
|
4540
5102
|
platform.browser = 'Edge';
|
|
4541
|
-
const match = ua.match(/Edg\/([\d
|
|
5103
|
+
const match = ua.match(/Edg\/([\d.]+)/);
|
|
4542
5104
|
if (match)
|
|
4543
5105
|
platform.browserVersion = match[1];
|
|
4544
5106
|
}
|
|
4545
5107
|
else if (/Chrome\//i.test(ua) && !/Edg/i.test(ua)) {
|
|
4546
5108
|
platform.browser = 'Chrome';
|
|
4547
|
-
const match = ua.match(/Chrome\/([\d
|
|
5109
|
+
const match = ua.match(/Chrome\/([\d.]+)/);
|
|
4548
5110
|
if (match)
|
|
4549
5111
|
platform.browserVersion = match[1];
|
|
4550
5112
|
}
|
|
4551
5113
|
else if (/Safari\//i.test(ua) && !/Chrome/i.test(ua)) {
|
|
4552
5114
|
platform.browser = 'Safari';
|
|
4553
|
-
const match = ua.match(/Version\/([\d
|
|
5115
|
+
const match = ua.match(/Version\/([\d.]+)/);
|
|
4554
5116
|
if (match)
|
|
4555
5117
|
platform.browserVersion = match[1];
|
|
4556
5118
|
}
|
|
4557
5119
|
else if (/Firefox\//i.test(ua)) {
|
|
4558
5120
|
platform.browser = 'Firefox';
|
|
4559
|
-
const match = ua.match(/Firefox\/([\d
|
|
5121
|
+
const match = ua.match(/Firefox\/([\d.]+)/);
|
|
4560
5122
|
if (match)
|
|
4561
5123
|
platform.browserVersion = match[1];
|
|
4562
5124
|
}
|
|
4563
5125
|
else if (/Opera|OPR\//i.test(ua)) {
|
|
4564
5126
|
platform.browser = 'Opera';
|
|
4565
|
-
const match = ua.match(/(?:Opera|OPR)\/([\d
|
|
5127
|
+
const match = ua.match(/(?:Opera|OPR)\/([\d.]+)/);
|
|
4566
5128
|
if (match)
|
|
4567
5129
|
platform.browserVersion = match[1];
|
|
4568
5130
|
}
|
|
@@ -4649,6 +5211,28 @@ function parseUtmParams(url) {
|
|
|
4649
5211
|
return params;
|
|
4650
5212
|
}
|
|
4651
5213
|
|
|
5214
|
+
/** Default page when none is specified. */
|
|
5215
|
+
const DEFAULT_PAGE = 1;
|
|
5216
|
+
/** Default page size when none is specified. */
|
|
5217
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
5218
|
+
/**
|
|
5219
|
+
* Build a URLSearchParams object from pagination options.
|
|
5220
|
+
*
|
|
5221
|
+
* Always includes page/limit so the server always receives explicit values.
|
|
5222
|
+
* Uses `??` (not `||`) so an explicit `page: 0` / `limit: 0` is preserved
|
|
5223
|
+
* rather than silently replaced by the default.
|
|
5224
|
+
*/
|
|
5225
|
+
function buildPaginationParams(options) {
|
|
5226
|
+
const params = new URLSearchParams();
|
|
5227
|
+
params.set('page', String(options?.page ?? DEFAULT_PAGE));
|
|
5228
|
+
params.set('limit', String(options?.limit ?? DEFAULT_PAGE_SIZE));
|
|
5229
|
+
if (options?.sortBy)
|
|
5230
|
+
params.set('sortBy', options.sortBy);
|
|
5231
|
+
if (options?.sortOrder)
|
|
5232
|
+
params.set('sortOrder', options.sortOrder);
|
|
5233
|
+
return params;
|
|
5234
|
+
}
|
|
5235
|
+
|
|
4652
5236
|
/**
|
|
4653
5237
|
* Analytics Constants
|
|
4654
5238
|
*
|
|
@@ -4777,6 +5361,53 @@ function isSupportedImageFormat(extension) {
|
|
|
4777
5361
|
return IMAGE_CDN_SUPPORTED_FORMATS.includes(extension.toLowerCase());
|
|
4778
5362
|
}
|
|
4779
5363
|
|
|
5364
|
+
/**
|
|
5365
|
+
* Chain RPC Permissions
|
|
5366
|
+
*
|
|
5367
|
+
* Canonical permission sets for BLOCKCHAIN_READER_JWT / BLOCKCHAIN_WRITER_JWT tokens
|
|
5368
|
+
* issued to authenticate against private chain RPC endpoints (e.g. PERS Besu).
|
|
5369
|
+
* Single source of truth shared between Web3ChainDomainService (NestJS app) and any
|
|
5370
|
+
* raw, no-NestJS Lambda that needs to self-issue the same token (e.g.
|
|
5371
|
+
* serverless-functions/infrastructure/chain-provider.ts) — keeps both in sync instead
|
|
5372
|
+
* of letting a hand-picked subset drift and silently 401 on RPC calls it never
|
|
5373
|
+
* anticipated needing.
|
|
5374
|
+
*/
|
|
5375
|
+
const CHAIN_READ_PERMISSIONS = [
|
|
5376
|
+
'eth:call',
|
|
5377
|
+
'eth:chainId',
|
|
5378
|
+
'eth:getTransactionCount',
|
|
5379
|
+
'eth:estimateGas',
|
|
5380
|
+
'eth:gasPrice',
|
|
5381
|
+
'eth:getBalance',
|
|
5382
|
+
'eth:getBlockByNumber',
|
|
5383
|
+
'eth:getBlockByHash',
|
|
5384
|
+
'eth:getBlockTransactionCountByHash',
|
|
5385
|
+
'eth:getBlockTransactionCountByNumber',
|
|
5386
|
+
'eth:getCode',
|
|
5387
|
+
'eth:getTransactionByBlockHashAndIndex',
|
|
5388
|
+
'eth:getTransactionByBlockNumberAndIndex',
|
|
5389
|
+
'eth:getTransactionByHash',
|
|
5390
|
+
'eth:getTransactionReceipt',
|
|
5391
|
+
'eth:subscribe',
|
|
5392
|
+
'eth:unsubscribe',
|
|
5393
|
+
];
|
|
5394
|
+
const CHAIN_WRITE_PERMISSIONS = [
|
|
5395
|
+
'eth:*',
|
|
5396
|
+
];
|
|
5397
|
+
|
|
5398
|
+
const CUSTOM_FIELD_TRANSLATABLE_FIELDS = ['label', 'description'];
|
|
5399
|
+
const USER_STATUS_TYPE_TRANSLATABLE_FIELDS = ['name', 'description'];
|
|
5400
|
+
const BUSINESS_TYPE_TRANSLATABLE_FIELDS = ['name', 'description'];
|
|
5401
|
+
const BUSINESS_TRANSLATABLE_FIELDS = ['displayName', 'description', 'shortDescription'];
|
|
5402
|
+
const TOKEN_METADATA_TRANSLATABLE_FIELDS = ['name', 'description'];
|
|
5403
|
+
const CAMPAIGN_TRIGGER_TRANSLATABLE_FIELDS = ['name', 'description', 'terms'];
|
|
5404
|
+
const TRIGGER_SOURCE_TRANSLATABLE_FIELDS = ['name', 'description'];
|
|
5405
|
+
const CAMPAIGN_TRANSLATABLE_FIELDS = ['name', 'description'];
|
|
5406
|
+
const REDEMPTION_TYPE_TRANSLATABLE_FIELDS = ['name', 'description'];
|
|
5407
|
+
const REDEMPTION_TRANSLATABLE_FIELDS = ['name', 'description', 'terms'];
|
|
5408
|
+
const TENANT_TRANSLATABLE_FIELDS = ['projectDescription'];
|
|
5409
|
+
const PURCHASE_TOKEN_TRANSLATABLE_FIELDS = ['description'];
|
|
5410
|
+
|
|
4780
5411
|
/**
|
|
4781
5412
|
* Chain Data Types
|
|
4782
5413
|
*
|
|
@@ -4850,6 +5481,8 @@ const CommonErrorCodes = {
|
|
|
4850
5481
|
// Business domain
|
|
4851
5482
|
BUSINESS_NOT_FOUND: 'BUSINESS_NOT_FOUND',
|
|
4852
5483
|
BUSINESS_NOT_MINTER: 'BUSINESS_NOT_MINTER',
|
|
5484
|
+
BUSINESS_DISPLAY_NAME_ALREADY_EXISTS: 'BUSINESS_DISPLAY_NAME_ALREADY_EXISTS',
|
|
5485
|
+
BUSINESS_NOT_APPROVED: 'BUSINESS_NOT_APPROVED',
|
|
4853
5486
|
// Membership domain
|
|
4854
5487
|
MEMBERSHIP_NOT_FOUND: 'MEMBERSHIP_NOT_FOUND',
|
|
4855
5488
|
MEMBERSHIP_ALREADY_EXISTS: 'MEMBERSHIP_ALREADY_EXISTS',
|
|
@@ -4867,6 +5500,7 @@ const CommonErrorCodes = {
|
|
|
4867
5500
|
CAMPAIGN_BUSINESS_REQUIRED: 'CAMPAIGN_BUSINESS_REQUIRED',
|
|
4868
5501
|
CAMPAIGN_CONDITION_NOT_MET: 'CAMPAIGN_CONDITION_NOT_MET',
|
|
4869
5502
|
CAMPAIGN_ALREADY_CLAIMED: 'CAMPAIGN_ALREADY_CLAIMED',
|
|
5503
|
+
CAMPAIGN_CLAIM_ALREADY_PROCESSING: 'CAMPAIGN_CLAIM_ALREADY_PROCESSING',
|
|
4870
5504
|
CAMPAIGN_NO_TRIGGER: 'CAMPAIGN_NO_TRIGGER',
|
|
4871
5505
|
CAMPAIGN_LOCATION_REQUIRED: 'CAMPAIGN_LOCATION_REQUIRED',
|
|
4872
5506
|
CAMPAIGN_DISTANCE_EXCEEDED: 'CAMPAIGN_DISTANCE_EXCEEDED',
|
|
@@ -4883,6 +5517,8 @@ const CommonErrorCodes = {
|
|
|
4883
5517
|
LOCATION_VERIFICATION_REQUIRED: 'LOCATION_VERIFICATION_REQUIRED',
|
|
4884
5518
|
// Trigger domain
|
|
4885
5519
|
TRIGGER_NOT_FOUND: 'TRIGGER_NOT_FOUND',
|
|
5520
|
+
// Notification domain
|
|
5521
|
+
NOTIFICATION_NOT_FOUND: 'NOTIFICATION_NOT_FOUND',
|
|
4886
5522
|
// Trigger Source domain
|
|
4887
5523
|
TRIGGER_SOURCE_NOT_FOUND: 'TRIGGER_SOURCE_NOT_FOUND',
|
|
4888
5524
|
// Generic domain errors
|
|
@@ -4896,10 +5532,15 @@ const CommonErrorCodes = {
|
|
|
4896
5532
|
EXTERNAL_SERVICE_ERROR: 'EXTERNAL_SERVICE_ERROR',
|
|
4897
5533
|
AUTHENTICATION_REQUIRED: 'AUTHENTICATION_REQUIRED',
|
|
4898
5534
|
AUTHORIZATION_FAILED: 'AUTHORIZATION_FAILED',
|
|
5535
|
+
NO_BUSINESS_ACCESS: 'NO_BUSINESS_ACCESS',
|
|
5536
|
+
USER_INACTIVE: 'USER_INACTIVE',
|
|
4899
5537
|
CONFIGURATION_MISSING: 'CONFIGURATION_MISSING',
|
|
4900
5538
|
// Redemption domain
|
|
4901
5539
|
USER_STATUS_RESTRICTED: 'USER_STATUS_RESTRICTED',
|
|
4902
5540
|
REDEMPTION_ALREADY_PROCESSING: 'REDEMPTION_ALREADY_PROCESSING',
|
|
5541
|
+
REDEMPTION_NOT_ACTIVE: 'REDEMPTION_NOT_ACTIVE',
|
|
5542
|
+
REDEMPTION_ENDED: 'REDEMPTION_ENDED',
|
|
5543
|
+
REDEMPTION_NOT_STARTED: 'REDEMPTION_NOT_STARTED',
|
|
4903
5544
|
MAX_REDEMPTION: 'MAX_REDEMPTION',
|
|
4904
5545
|
USER_LIMIT: 'USER_LIMIT',
|
|
4905
5546
|
REQUIRED_FIELDS_MISSING: 'REQUIRED_FIELDS_MISSING',
|
|
@@ -4931,6 +5572,8 @@ const CommonErrorCodes = {
|
|
|
4931
5572
|
WEBHOOK_SIGNATURE_INVALID: 'WEBHOOK_SIGNATURE_INVALID',
|
|
4932
5573
|
WEBHOOK_FORWARD_FAILED: 'WEBHOOK_FORWARD_FAILED',
|
|
4933
5574
|
WEBHOOK_TIMEOUT: 'WEBHOOK_TIMEOUT',
|
|
5575
|
+
// Events domain
|
|
5576
|
+
EVENT_SUBSCRIPTION_NOT_FOUND: 'EVENT_SUBSCRIPTION_NOT_FOUND',
|
|
4934
5577
|
// Conflict errors (409)
|
|
4935
5578
|
RESOURCE_CONFLICT: 'RESOURCE_CONFLICT',
|
|
4936
5579
|
CONCURRENT_MODIFICATION: 'CONCURRENT_MODIFICATION',
|
|
@@ -5525,39 +6168,11 @@ const CacheTTL = {
|
|
|
5525
6168
|
PROVIDER: 60 * 60 * 1000, // 1 hour - Provider connections with auth
|
|
5526
6169
|
};
|
|
5527
6170
|
|
|
5528
|
-
//
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
* Ensures pagination params are always included in API requests
|
|
5534
|
-
*
|
|
5535
|
-
* @param options - Pagination options (optional)
|
|
5536
|
-
* @returns URLSearchParams with page and limit always set
|
|
5537
|
-
*
|
|
5538
|
-
* @example
|
|
5539
|
-
* ```typescript
|
|
5540
|
-
* async getItems(options?: PaginationOptions): Promise<PaginatedResponseDTO<ItemDTO>> {
|
|
5541
|
-
* const params = buildPaginationParams(options);
|
|
5542
|
-
* // params always includes: page=1&limit=50 (or custom values)
|
|
5543
|
-
*
|
|
5544
|
-
* if (options?.sortBy) params.set('sortBy', options.sortBy);
|
|
5545
|
-
* if (options?.sortOrder) params.set('sortOrder', options.sortOrder);
|
|
5546
|
-
*
|
|
5547
|
-
* return this.apiClient.get(`/items?${params.toString()}`);
|
|
5548
|
-
* }
|
|
5549
|
-
* ```
|
|
5550
|
-
*/
|
|
5551
|
-
function buildPaginationParams(options) {
|
|
5552
|
-
const params = new URLSearchParams();
|
|
5553
|
-
// Always include pagination params (defaults)
|
|
5554
|
-
params.set('page', String(options?.page ?? 1));
|
|
5555
|
-
params.set('limit', String(options?.limit ?? 50));
|
|
5556
|
-
if (options?.sortBy)
|
|
5557
|
-
params.set('sortBy', options.sortBy);
|
|
5558
|
-
if (options?.sortOrder)
|
|
5559
|
-
params.set('sortOrder', options.sortOrder);
|
|
5560
|
-
return params;
|
|
6171
|
+
// Re-export pagination primitives from @explorins/pers-shared (single source of truth)
|
|
6172
|
+
/** Sets the comma-separated `include` query parameter when there is anything to include. */
|
|
6173
|
+
function setIncludeParam(params, include) {
|
|
6174
|
+
if (include?.length)
|
|
6175
|
+
params.set('include', include.join(','));
|
|
5561
6176
|
}
|
|
5562
6177
|
/**
|
|
5563
6178
|
* Type guard to check if response is paginated
|
|
@@ -5676,7 +6291,7 @@ function normalizeToPaginated(response) {
|
|
|
5676
6291
|
* );
|
|
5677
6292
|
* ```
|
|
5678
6293
|
*/
|
|
5679
|
-
async function fetchAllPages(fetchFn, limit =
|
|
6294
|
+
async function fetchAllPages(fetchFn, limit = DEFAULT_PAGE_SIZE, maxPages = 100) {
|
|
5680
6295
|
const allItems = [];
|
|
5681
6296
|
let page = 1;
|
|
5682
6297
|
let hasMore = true;
|
|
@@ -5728,6 +6343,13 @@ class UserApi {
|
|
|
5728
6343
|
const response = await this.apiClient.get(url);
|
|
5729
6344
|
return normalizeToPaginated(response);
|
|
5730
6345
|
}
|
|
6346
|
+
/**
|
|
6347
|
+
* PUBLIC: Get a single user's public profile by ID
|
|
6348
|
+
* Uses RESTful /users/public/:id endpoint
|
|
6349
|
+
*/
|
|
6350
|
+
async getPublicProfileById(id) {
|
|
6351
|
+
return this.apiClient.get(`${this.basePath}/public/${id}`);
|
|
6352
|
+
}
|
|
5731
6353
|
// ==========================================
|
|
5732
6354
|
// AUTHENTICATED OPERATIONS
|
|
5733
6355
|
// ==========================================
|
|
@@ -5936,6 +6558,12 @@ class UserService {
|
|
|
5936
6558
|
async getAllUsersPublicProfiles(filter = null, options) {
|
|
5937
6559
|
return this.userApi.getAllUsersPublicProfiles(filter, options);
|
|
5938
6560
|
}
|
|
6561
|
+
/**
|
|
6562
|
+
* PUBLIC: Get a single user's public profile by ID
|
|
6563
|
+
*/
|
|
6564
|
+
async getPublicProfileById(id) {
|
|
6565
|
+
return this.userApi.getPublicProfileById(id);
|
|
6566
|
+
}
|
|
5939
6567
|
// ==========================================
|
|
5940
6568
|
// AUTHENTICATED OPERATIONS
|
|
5941
6569
|
// ==========================================
|
|
@@ -6067,6 +6695,7 @@ class UserStatusApi {
|
|
|
6067
6695
|
async getRemoteUserStatusTypes(options) {
|
|
6068
6696
|
try {
|
|
6069
6697
|
const params = buildPaginationParams(options);
|
|
6698
|
+
setIncludeParam(params, options?.include);
|
|
6070
6699
|
const response = await this.apiClient.get(`${this.basePath}/status-types?${params.toString()}`);
|
|
6071
6700
|
return normalizeToPaginated(response);
|
|
6072
6701
|
}
|
|
@@ -6075,13 +6704,18 @@ class UserStatusApi {
|
|
|
6075
6704
|
throw error;
|
|
6076
6705
|
}
|
|
6077
6706
|
}
|
|
6707
|
+
/** PUBLIC: Get one user status type; `translations` adds the raw per-locale map (admin editing). */
|
|
6708
|
+
async getRemoteUserStatusTypeById(id, include) {
|
|
6709
|
+
const params = include?.length ? `?include=${include.join(',')}` : '';
|
|
6710
|
+
return this.apiClient.get(`${this.basePath}/status-types/${id}${params}`);
|
|
6711
|
+
}
|
|
6078
6712
|
// ==========================================
|
|
6079
6713
|
// AUTHENTICATED OPERATIONS
|
|
6080
6714
|
// ==========================================
|
|
6081
6715
|
/**
|
|
6082
6716
|
* AUTH: Get earned user status for authenticated user
|
|
6083
6717
|
* ✅ UPDATED: /user/auth/status → /users/me/status
|
|
6084
|
-
* ✅ FIXED: Returns PaginatedResponseDTO<UserStatusTypeDTO
|
|
6718
|
+
* ✅ FIXED: Returns `PaginatedResponseDTO<UserStatusTypeDTO>` to match v2.0.0
|
|
6085
6719
|
* Note: Uses silent fallback pattern from framework - returns empty paginated response on error
|
|
6086
6720
|
*/
|
|
6087
6721
|
async getRemoteEarnedUserStatus(options) {
|
|
@@ -6163,6 +6797,9 @@ class UserStatusService {
|
|
|
6163
6797
|
async getRemoteUserStatusTypes(options) {
|
|
6164
6798
|
return this.userStatusApi.getRemoteUserStatusTypes(options);
|
|
6165
6799
|
}
|
|
6800
|
+
async getRemoteUserStatusTypeById(id, include) {
|
|
6801
|
+
return this.userStatusApi.getRemoteUserStatusTypeById(id, include);
|
|
6802
|
+
}
|
|
6166
6803
|
// ==========================================
|
|
6167
6804
|
// AUTHENTICATED OPERATIONS
|
|
6168
6805
|
// ==========================================
|
|
@@ -6216,6 +6853,7 @@ function createUserStatusSDK(apiClient) {
|
|
|
6216
6853
|
// ✅ FRAMEWORK ALIGNED: Only methods actually used by framework
|
|
6217
6854
|
// Public methods
|
|
6218
6855
|
getRemoteUserStatusTypes: (options) => userStatusService.getRemoteUserStatusTypes(options),
|
|
6856
|
+
getRemoteUserStatusTypeById: (id, include) => userStatusService.getRemoteUserStatusTypeById(id, include),
|
|
6219
6857
|
// Auth methods
|
|
6220
6858
|
getRemoteEarnedUserStatus: (options) => userStatusService.getRemoteEarnedUserStatus(options),
|
|
6221
6859
|
// Admin methods
|
|
@@ -6353,6 +6991,9 @@ class TokenApi {
|
|
|
6353
6991
|
if (options?.search) {
|
|
6354
6992
|
params.set('search', options.search);
|
|
6355
6993
|
}
|
|
6994
|
+
if (options?.approvalStatus) {
|
|
6995
|
+
params.set('approvalStatus', options.approvalStatus);
|
|
6996
|
+
}
|
|
6356
6997
|
// Handle tags as string or array
|
|
6357
6998
|
if (options?.tags) {
|
|
6358
6999
|
const tagsArray = Array.isArray(options.tags) ? options.tags : [options.tags];
|
|
@@ -6374,6 +7015,15 @@ class TokenApi {
|
|
|
6374
7015
|
// ==========================================
|
|
6375
7016
|
// ADMIN OPERATIONS
|
|
6376
7017
|
// ==========================================
|
|
7018
|
+
/**
|
|
7019
|
+
* ADMIN/BUSINESS: Get one token metadata (a business can only read its own)
|
|
7020
|
+
*
|
|
7021
|
+
* @param include - Same relations as the list: `translations` adds the raw per-locale map (admin editing)
|
|
7022
|
+
*/
|
|
7023
|
+
async getTokenMetadataById(metadataId, include) {
|
|
7024
|
+
const params = include?.length ? `?include=${include.join(',')}` : '';
|
|
7025
|
+
return this.apiClient.get(`${this.basePath}/metadata/${metadataId}${params}`);
|
|
7026
|
+
}
|
|
6377
7027
|
/**
|
|
6378
7028
|
* ADMIN: Create new token
|
|
6379
7029
|
*/
|
|
@@ -6452,7 +7102,7 @@ class TokenApi {
|
|
|
6452
7102
|
return this.apiClient.delete(`${this.basePath}/metadata/${metadataId}`);
|
|
6453
7103
|
}
|
|
6454
7104
|
// ==========================================
|
|
6455
|
-
//
|
|
7105
|
+
// 📊CSV EXPORT
|
|
6456
7106
|
// ==========================================
|
|
6457
7107
|
/**
|
|
6458
7108
|
* ADMIN: Export token metadata to CSV
|
|
@@ -6488,6 +7138,23 @@ class TokenApi {
|
|
|
6488
7138
|
const url = `${this.basePath}/metadata/export/csv${queryString ? `?${queryString}` : ''}`;
|
|
6489
7139
|
return this.apiClient.get(url, 'blob');
|
|
6490
7140
|
}
|
|
7141
|
+
// ==========================================
|
|
7142
|
+
// ✅ APPROVAL WORKFLOW
|
|
7143
|
+
// ==========================================
|
|
7144
|
+
/**
|
|
7145
|
+
* ADMIN: Approve a token metadata
|
|
7146
|
+
*
|
|
7147
|
+
* Approves token metadata that is pending approval, changing its status to 'approved'.
|
|
7148
|
+
*
|
|
7149
|
+
* Endpoint: PATCH /admin/token-metadata/:id/approve
|
|
7150
|
+
* Auth: @TenantAdmin()
|
|
7151
|
+
*
|
|
7152
|
+
* @param tokenMetadataId - ID of the token metadata to approve
|
|
7153
|
+
* @returns Updated token metadata with approval metadata
|
|
7154
|
+
*/
|
|
7155
|
+
async setTokenMetadataApproval(tokenMetadataId, status, reason) {
|
|
7156
|
+
return this.apiClient.patch(`/tokens/metadata/${tokenMetadataId}/approval`, { status, reason });
|
|
7157
|
+
}
|
|
6491
7158
|
}
|
|
6492
7159
|
|
|
6493
7160
|
/**
|
|
@@ -6561,6 +7228,12 @@ class TokenService {
|
|
|
6561
7228
|
async getTokenMetadata(options) {
|
|
6562
7229
|
return this.tokenApi.getTokenMetadata(options);
|
|
6563
7230
|
}
|
|
7231
|
+
/**
|
|
7232
|
+
* Get one token metadata by ID (admin/business), with the same include relations as the list
|
|
7233
|
+
*/
|
|
7234
|
+
async getTokenMetadataById(metadataId, include) {
|
|
7235
|
+
return this.tokenApi.getTokenMetadataById(metadataId, include);
|
|
7236
|
+
}
|
|
6564
7237
|
/**
|
|
6565
7238
|
* Get rewards (ERC1155 token metadata) with filtering, pagination, and include relations
|
|
6566
7239
|
*
|
|
@@ -6692,7 +7365,7 @@ class BusinessApi {
|
|
|
6692
7365
|
this.businessTypesPath = '/businesses/types';
|
|
6693
7366
|
}
|
|
6694
7367
|
// ==========================================
|
|
6695
|
-
//
|
|
7368
|
+
// 🌠BUSINESS TYPES MANAGEMENT
|
|
6696
7369
|
// ==========================================
|
|
6697
7370
|
/**
|
|
6698
7371
|
* Get all business types (project key required)
|
|
@@ -6705,9 +7378,15 @@ class BusinessApi {
|
|
|
6705
7378
|
*/
|
|
6706
7379
|
async getBusinessTypes(options) {
|
|
6707
7380
|
const params = buildPaginationParams(options);
|
|
7381
|
+
setIncludeParam(params, options?.include);
|
|
6708
7382
|
const response = await this.apiClient.get(`${this.businessTypesPath}?${params.toString()}`);
|
|
6709
7383
|
return normalizeToPaginated(response);
|
|
6710
7384
|
}
|
|
7385
|
+
/** PUBLIC: Get one business type; `translations` adds the raw per-locale map (admin editing). */
|
|
7386
|
+
async getBusinessTypeById(id, include) {
|
|
7387
|
+
const params = include?.length ? `?include=${include.join(',')}` : '';
|
|
7388
|
+
return this.apiClient.get(`${this.businessTypesPath}/${id}${params}`);
|
|
7389
|
+
}
|
|
6711
7390
|
/**
|
|
6712
7391
|
* ADMIN: Create business type
|
|
6713
7392
|
*
|
|
@@ -6736,7 +7415,7 @@ class BusinessApi {
|
|
|
6736
7415
|
return this.apiClient.delete(`${this.businessTypesPath}/${id}`);
|
|
6737
7416
|
}
|
|
6738
7417
|
// ==========================================
|
|
6739
|
-
//
|
|
7418
|
+
// ðŸÂ¢ BUSINESS MANAGEMENT
|
|
6740
7419
|
// ==========================================
|
|
6741
7420
|
/**
|
|
6742
7421
|
* Get current business info (business authentication required)
|
|
@@ -6786,12 +7465,15 @@ class BusinessApi {
|
|
|
6786
7465
|
*/
|
|
6787
7466
|
async getBusinesses(options) {
|
|
6788
7467
|
const params = buildPaginationParams(options);
|
|
7468
|
+
setIncludeParam(params, options?.include);
|
|
6789
7469
|
if (options?.active !== undefined) {
|
|
6790
7470
|
params.set('active', String(options.active));
|
|
6791
7471
|
}
|
|
6792
7472
|
if (options?.search) {
|
|
6793
7473
|
params.set('search', options.search);
|
|
6794
7474
|
}
|
|
7475
|
+
if (options?.approvalStatus)
|
|
7476
|
+
params.set('approvalStatus', options.approvalStatus);
|
|
6795
7477
|
// Support single tag or array of tags (multiple ?tags=a&tags=b)
|
|
6796
7478
|
if (options?.tags) {
|
|
6797
7479
|
const tagsArray = Array.isArray(options.tags) ? options.tags : [options.tags];
|
|
@@ -6801,6 +7483,12 @@ class BusinessApi {
|
|
|
6801
7483
|
if (options?.sanitize) {
|
|
6802
7484
|
params.set('sanitize', options.sanitize);
|
|
6803
7485
|
}
|
|
7486
|
+
// Batch lookup by id: one comma-separated param keeps the URL short for large id lists
|
|
7487
|
+
if (options?.ids) {
|
|
7488
|
+
const ids = Array.isArray(options.ids) ? options.ids : [options.ids];
|
|
7489
|
+
if (ids.length > 0)
|
|
7490
|
+
params.set('ids', ids.join(','));
|
|
7491
|
+
}
|
|
6804
7492
|
if (options?.businessTypeId !== undefined) {
|
|
6805
7493
|
params.set('businessTypeId', String(options.businessTypeId));
|
|
6806
7494
|
}
|
|
@@ -6820,17 +7508,74 @@ class BusinessApi {
|
|
|
6820
7508
|
if (options?.district) {
|
|
6821
7509
|
params.set('district', options.district);
|
|
6822
7510
|
}
|
|
7511
|
+
// Geographic radius search parameters
|
|
7512
|
+
if (options?.latitude !== undefined) {
|
|
7513
|
+
params.set('latitude', String(options.latitude));
|
|
7514
|
+
}
|
|
7515
|
+
if (options?.longitude !== undefined) {
|
|
7516
|
+
params.set('longitude', String(options.longitude));
|
|
7517
|
+
}
|
|
7518
|
+
if (options?.radiusMeters !== undefined) {
|
|
7519
|
+
params.set('radiusMeters', String(options.radiusMeters));
|
|
7520
|
+
}
|
|
7521
|
+
// Capability flag filters (admin only - ignored by non-admin callers server-side)
|
|
7522
|
+
if (options?.canMintToken !== undefined) {
|
|
7523
|
+
params.set('canMintToken', String(options.canMintToken));
|
|
7524
|
+
}
|
|
7525
|
+
if (options?.canChargeToken !== undefined) {
|
|
7526
|
+
params.set('canChargeToken', String(options.canChargeToken));
|
|
7527
|
+
}
|
|
7528
|
+
if (options?.canManageUsers !== undefined) {
|
|
7529
|
+
params.set('canManageUsers', String(options.canManageUsers));
|
|
7530
|
+
}
|
|
7531
|
+
if (options?.canReceiveDonation !== undefined) {
|
|
7532
|
+
params.set('canReceiveDonation', String(options.canReceiveDonation));
|
|
7533
|
+
}
|
|
6823
7534
|
const response = await this.apiClient.get(`${this.basePath}?${params.toString()}`);
|
|
6824
7535
|
return normalizeToPaginated(response);
|
|
6825
7536
|
}
|
|
7537
|
+
/**
|
|
7538
|
+
* Get the distinct list of cities that have at least one business, for populating
|
|
7539
|
+
* city filter options before calling `getBusinesses({ city })`. Each option includes
|
|
7540
|
+
* `country`/`countryCode` so same-named cities in different countries can be disambiguated.
|
|
7541
|
+
*
|
|
7542
|
+
* Endpoint: GET /businesses/cities
|
|
7543
|
+
* Auth: @ApiSecurity('projectKey') (enhanced with role-based filtering, same as getBusinesses)
|
|
7544
|
+
*
|
|
7545
|
+
* @example
|
|
7546
|
+
* ```typescript
|
|
7547
|
+
* const cities = await businessApi.getBusinessCities();
|
|
7548
|
+
* // [{ city: 'Doha', country: 'Qatar', countryCode: 'QA' }, ...]
|
|
7549
|
+
*
|
|
7550
|
+
* const citiesInQatar = await businessApi.getBusinessCities({ countryCode: 'QA' });
|
|
7551
|
+
* ```
|
|
7552
|
+
*/
|
|
7553
|
+
async getBusinessCities(options) {
|
|
7554
|
+
const params = new URLSearchParams();
|
|
7555
|
+
if (options?.active !== undefined)
|
|
7556
|
+
params.set('active', String(options.active));
|
|
7557
|
+
if (options?.approvalStatus)
|
|
7558
|
+
params.set('approvalStatus', options.approvalStatus);
|
|
7559
|
+
if (options?.countryCode)
|
|
7560
|
+
params.set('countryCode', options.countryCode);
|
|
7561
|
+
if (options?.country)
|
|
7562
|
+
params.set('country', options.country);
|
|
7563
|
+
if (options?.businessTypeId !== undefined)
|
|
7564
|
+
params.set('businessTypeId', String(options.businessTypeId));
|
|
7565
|
+
const query = params.toString();
|
|
7566
|
+
return this.apiClient.get(`${this.basePath}/cities${query ? `?${query}` : ''}`);
|
|
7567
|
+
}
|
|
6826
7568
|
/**
|
|
6827
7569
|
* Get business by ID
|
|
6828
7570
|
*
|
|
6829
7571
|
* Endpoint: GET /businesses/{id}
|
|
6830
7572
|
* Auth: @ApiSecurity('projectKey')
|
|
6831
7573
|
*/
|
|
6832
|
-
async getBusinessById(businessId) {
|
|
6833
|
-
|
|
7574
|
+
async getBusinessById(businessId, include) {
|
|
7575
|
+
const params = new URLSearchParams();
|
|
7576
|
+
setIncludeParam(params, include);
|
|
7577
|
+
const query = params.toString();
|
|
7578
|
+
return this.apiClient.get(`${this.basePath}/${businessId}${query ? `?${query}` : ''}`);
|
|
6834
7579
|
}
|
|
6835
7580
|
/**
|
|
6836
7581
|
* Get business by account address
|
|
@@ -6842,14 +7587,14 @@ class BusinessApi {
|
|
|
6842
7587
|
return this.apiClient.get(`${this.basePath}/account/${accountAddress}`);
|
|
6843
7588
|
}
|
|
6844
7589
|
// ==========================================
|
|
6845
|
-
//
|
|
7590
|
+
// ðŸâ€Â§ ADMIN OPERATIONS
|
|
6846
7591
|
// ==========================================
|
|
6847
7592
|
/**
|
|
6848
7593
|
* ADMIN: Create business
|
|
6849
7594
|
*
|
|
6850
7595
|
* Endpoint: POST /businesses
|
|
6851
7596
|
* Auth: @TenantAdmin()
|
|
6852
|
-
* Returns: BusinessApiKeyDTO
|
|
7597
|
+
* Returns: BusinessApiKeyDTO
|
|
6853
7598
|
*/
|
|
6854
7599
|
async createBusiness(dto) {
|
|
6855
7600
|
return this.apiClient.post(this.basePath, dto);
|
|
@@ -6904,7 +7649,7 @@ class BusinessApi {
|
|
|
6904
7649
|
// return this.apiClient.put<string>(`${this.basePath}/${id}/api-key`, {});
|
|
6905
7650
|
// }
|
|
6906
7651
|
// ==========================================
|
|
6907
|
-
//
|
|
7652
|
+
// 📊CSV EXPORT
|
|
6908
7653
|
// ==========================================
|
|
6909
7654
|
/**
|
|
6910
7655
|
* ADMIN: Export businesses to CSV
|
|
@@ -6930,6 +7675,31 @@ class BusinessApi {
|
|
|
6930
7675
|
* });
|
|
6931
7676
|
* ```
|
|
6932
7677
|
*/
|
|
7678
|
+
// ==========================================
|
|
7679
|
+
// ✅ APPROVAL WORKFLOW
|
|
7680
|
+
// ==========================================
|
|
7681
|
+
/**
|
|
7682
|
+
* ADMIN: Approve a business
|
|
7683
|
+
*
|
|
7684
|
+
* Approves a business that is pending approval, changing its status to 'approved'.
|
|
7685
|
+
* Only businesses with approval status 'pending_approval' can be approved.
|
|
7686
|
+
*
|
|
7687
|
+
* Endpoint: PATCH /admin/businesses/:id/approve
|
|
7688
|
+
* Auth: @TenantAdmin()
|
|
7689
|
+
*
|
|
7690
|
+
* @param businessId - ID of the business to approve
|
|
7691
|
+
* @returns Updated business with approval metadata
|
|
7692
|
+
*
|
|
7693
|
+
* @example
|
|
7694
|
+
* ` ypescript
|
|
7695
|
+
* const approvedBusiness = await businessApi.approveBusiness('business-123');
|
|
7696
|
+
* console.log('Approved by:', approvedBusiness.approval?.approvedBy);
|
|
7697
|
+
* console.log('Approved at:', approvedBusiness.approval?.approvedAt);
|
|
7698
|
+
* `
|
|
7699
|
+
*/
|
|
7700
|
+
async setBusinessApproval(businessId, status, reason) {
|
|
7701
|
+
return this.apiClient.patch(`/businesses/${businessId}/approval`, { status, reason });
|
|
7702
|
+
}
|
|
6933
7703
|
async exportCSV(options) {
|
|
6934
7704
|
const params = new URLSearchParams();
|
|
6935
7705
|
if (options?.dateFrom)
|
|
@@ -7202,11 +7972,21 @@ class BusinessService {
|
|
|
7202
7972
|
async getBusinessTypes(options) {
|
|
7203
7973
|
return this.businessApi.getBusinessTypes(options);
|
|
7204
7974
|
}
|
|
7975
|
+
async getBusinessTypeById(id, include) {
|
|
7976
|
+
return this.businessApi.getBusinessTypeById(id, include);
|
|
7977
|
+
}
|
|
7205
7978
|
/**
|
|
7206
7979
|
* Get business by ID
|
|
7207
7980
|
*/
|
|
7208
|
-
async getBusinessById(businessId) {
|
|
7209
|
-
return this.businessApi.getBusinessById(businessId);
|
|
7981
|
+
async getBusinessById(businessId, include) {
|
|
7982
|
+
return this.businessApi.getBusinessById(businessId, include);
|
|
7983
|
+
}
|
|
7984
|
+
/**
|
|
7985
|
+
* Get the distinct list of cities that have at least one business, for populating
|
|
7986
|
+
* city filter options before calling `getBusinesses({ city })`.
|
|
7987
|
+
*/
|
|
7988
|
+
async getBusinessCities(options) {
|
|
7989
|
+
return this.businessApi.getBusinessCities(options);
|
|
7210
7990
|
}
|
|
7211
7991
|
/**
|
|
7212
7992
|
* Get business by account address
|
|
@@ -7237,11 +8017,18 @@ class BusinessService {
|
|
|
7237
8017
|
async getBusinesses(options) {
|
|
7238
8018
|
return this.businessApi.getBusinesses(options);
|
|
7239
8019
|
}
|
|
8020
|
+
/**
|
|
8021
|
+
* ADMIN: Create business with full DTO
|
|
8022
|
+
*/
|
|
8023
|
+
async createBusiness(dto) {
|
|
8024
|
+
return this.businessApi.createBusiness(dto);
|
|
8025
|
+
}
|
|
7240
8026
|
/**
|
|
7241
8027
|
* ADMIN: Create business by display name
|
|
8028
|
+
* @deprecated Use createBusiness(dto) for full control
|
|
7242
8029
|
*/
|
|
7243
8030
|
async createBusinessByDisplayName(displayName) {
|
|
7244
|
-
return this.businessApi.
|
|
8031
|
+
return this.businessApi.createBusiness({ displayName });
|
|
7245
8032
|
}
|
|
7246
8033
|
/**
|
|
7247
8034
|
* ADMIN: Update business
|
|
@@ -7573,36 +8360,32 @@ class CampaignApi {
|
|
|
7573
8360
|
* ```
|
|
7574
8361
|
*/
|
|
7575
8362
|
async getCampaigns(options) {
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
params.push(`page=${options?.page || 1}`);
|
|
7579
|
-
params.push(`limit=${options?.limit || 50}`);
|
|
8363
|
+
// Always include pagination params (page/limit, plus sortBy/sortOrder) to ensure a paginated response
|
|
8364
|
+
const params = buildPaginationParams(options);
|
|
7580
8365
|
if (options?.active !== undefined)
|
|
7581
|
-
params.
|
|
8366
|
+
params.set('active', String(options.active));
|
|
7582
8367
|
// Support single tag or array of tags (multiple ?tags=a&tags=b)
|
|
7583
8368
|
if (options?.tags) {
|
|
7584
8369
|
const tagsArray = Array.isArray(options.tags) ? options.tags : [options.tags];
|
|
7585
|
-
tagsArray.forEach(t => params.
|
|
8370
|
+
tagsArray.forEach(t => params.append('tags', t));
|
|
7586
8371
|
}
|
|
7587
8372
|
if (options?.businessId)
|
|
7588
|
-
params.
|
|
8373
|
+
params.set('businessId', options.businessId);
|
|
8374
|
+
if (options?.ownerBusinessId)
|
|
8375
|
+
params.set('ownerBusinessId', options.ownerBusinessId);
|
|
8376
|
+
if (options?.approvalStatus)
|
|
8377
|
+
params.set('approvalStatus', options.approvalStatus);
|
|
8378
|
+
if (options?.status)
|
|
8379
|
+
params.set('status', [options.status].flat().join(','));
|
|
7589
8380
|
if (options?.search)
|
|
7590
|
-
params.
|
|
7591
|
-
if (options?.startDate)
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
if (options?.endDate) {
|
|
7596
|
-
const dateStr = options.endDate instanceof Date ? options.endDate.toISOString() : options.endDate;
|
|
7597
|
-
params.push(`endDate=${encodeURIComponent(dateStr)}`);
|
|
7598
|
-
}
|
|
7599
|
-
if (options?.sortBy)
|
|
7600
|
-
params.push(`sortBy=${options.sortBy}`);
|
|
7601
|
-
if (options?.sortOrder)
|
|
7602
|
-
params.push(`sortOrder=${options.sortOrder}`);
|
|
8381
|
+
params.set('search', options.search);
|
|
8382
|
+
if (options?.startDate)
|
|
8383
|
+
params.set('startDate', options.startDate);
|
|
8384
|
+
if (options?.endDate)
|
|
8385
|
+
params.set('endDate', options.endDate);
|
|
7603
8386
|
if (options?.include?.length)
|
|
7604
|
-
params.
|
|
7605
|
-
return this.apiClient.get(`/campaigns?${params.
|
|
8387
|
+
params.set('include', options.include.join(','));
|
|
8388
|
+
return this.apiClient.get(`/campaigns?${params.toString()}`);
|
|
7606
8389
|
}
|
|
7607
8390
|
/**
|
|
7608
8391
|
* PUBLIC: Get campaign by ID
|
|
@@ -7869,6 +8652,10 @@ class CampaignApi {
|
|
|
7869
8652
|
params.set('userId', filters.userId);
|
|
7870
8653
|
if (filters?.businessId)
|
|
7871
8654
|
params.set('businessId', filters.businessId);
|
|
8655
|
+
if (filters?.triggerType)
|
|
8656
|
+
params.set('triggerType', filters.triggerType);
|
|
8657
|
+
if (filters?.status)
|
|
8658
|
+
params.set('status', filters.status);
|
|
7872
8659
|
if (filters?.dateFrom)
|
|
7873
8660
|
params.set('dateFrom', filters.dateFrom instanceof Date ? filters.dateFrom.toISOString() : filters.dateFrom);
|
|
7874
8661
|
if (filters?.dateTo)
|
|
@@ -7963,6 +8750,23 @@ class CampaignApi {
|
|
|
7963
8750
|
const url = `/campaigns/claims/export/csv${queryString ? `?${queryString}` : ''}`;
|
|
7964
8751
|
return this.apiClient.get(url, 'blob');
|
|
7965
8752
|
}
|
|
8753
|
+
// ==========================================
|
|
8754
|
+
// ✅ APPROVAL WORKFLOW
|
|
8755
|
+
// ==========================================
|
|
8756
|
+
/**
|
|
8757
|
+
* ADMIN: Approve a campaign
|
|
8758
|
+
*
|
|
8759
|
+
* Approves a campaign that is pending approval, changing its status to 'approved'.
|
|
8760
|
+
*
|
|
8761
|
+
* Endpoint: PATCH /admin/campaigns/:id/approve
|
|
8762
|
+
* Auth: @TenantAdmin()
|
|
8763
|
+
*
|
|
8764
|
+
* @param campaignId - ID of the campaign to approve
|
|
8765
|
+
* @returns Updated campaign with approval metadata
|
|
8766
|
+
*/
|
|
8767
|
+
async setCampaignApproval(campaignId, status, reason) {
|
|
8768
|
+
return this.apiClient.patch(`/campaigns/${campaignId}/approval`, { status, reason });
|
|
8769
|
+
}
|
|
7966
8770
|
}
|
|
7967
8771
|
|
|
7968
8772
|
/**
|
|
@@ -8197,52 +9001,54 @@ class RedemptionApi {
|
|
|
8197
9001
|
* ```
|
|
8198
9002
|
*/
|
|
8199
9003
|
async getRedemptions(options) {
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
params.push(`page=${options?.page || 1}`);
|
|
8203
|
-
params.push(`limit=${options?.limit || 50}`);
|
|
9004
|
+
// Always include pagination params (page/limit, plus sortBy/sortOrder) to ensure a paginated response
|
|
9005
|
+
const params = buildPaginationParams(options);
|
|
8204
9006
|
if (options?.active !== undefined)
|
|
8205
|
-
params.
|
|
9007
|
+
params.set('active', String(options.active));
|
|
8206
9008
|
if (options?.search)
|
|
8207
|
-
params.
|
|
9009
|
+
params.set('search', options.search);
|
|
8208
9010
|
// Support single tag or array of tags (multiple ?tags=a&tags=b)
|
|
8209
9011
|
if (options?.tags) {
|
|
8210
9012
|
const tagsArray = Array.isArray(options.tags) ? options.tags : [options.tags];
|
|
8211
|
-
tagsArray.forEach(t => params.
|
|
8212
|
-
}
|
|
8213
|
-
if (options?.startDate) {
|
|
8214
|
-
const dateStr = options.startDate instanceof Date ? options.startDate.toISOString() : options.startDate;
|
|
8215
|
-
params.push(`startDate=${encodeURIComponent(dateStr)}`);
|
|
8216
|
-
}
|
|
8217
|
-
if (options?.endDate) {
|
|
8218
|
-
const dateStr = options.endDate instanceof Date ? options.endDate.toISOString() : options.endDate;
|
|
8219
|
-
params.push(`endDate=${encodeURIComponent(dateStr)}`);
|
|
9013
|
+
tagsArray.forEach(t => params.append('tags', t));
|
|
8220
9014
|
}
|
|
8221
|
-
if (options?.
|
|
8222
|
-
params.
|
|
8223
|
-
if (options?.
|
|
8224
|
-
params.
|
|
9015
|
+
if (options?.ownerBusinessId)
|
|
9016
|
+
params.set('ownerBusinessId', options.ownerBusinessId);
|
|
9017
|
+
if (options?.approvalStatus)
|
|
9018
|
+
params.set('approvalStatus', options.approvalStatus);
|
|
9019
|
+
if (options?.status)
|
|
9020
|
+
params.set('status', [options.status].flat().join(','));
|
|
9021
|
+
if (options?.startDate)
|
|
9022
|
+
params.set('startDate', options.startDate);
|
|
9023
|
+
if (options?.endDate)
|
|
9024
|
+
params.set('endDate', options.endDate);
|
|
8225
9025
|
if (options?.include?.length)
|
|
8226
|
-
params.
|
|
8227
|
-
const response = await this.apiClient.get(`${this.basePath}?${params.
|
|
9026
|
+
params.set('include', options.include.join(','));
|
|
9027
|
+
const response = await this.apiClient.get(`${this.basePath}?${params.toString()}`);
|
|
8228
9028
|
return normalizeToPaginated(response);
|
|
8229
9029
|
}
|
|
8230
9030
|
/**
|
|
8231
9031
|
* PUBLIC: Get redemption types
|
|
8232
9032
|
*
|
|
8233
|
-
* Updated: /redemption/type
|
|
9033
|
+
* Updated: /redemption/type → /redemption-types
|
|
8234
9034
|
* @param options Pagination options (page, limit, sortBy, sortOrder)
|
|
8235
9035
|
* @returns Paginated response with redemption types
|
|
8236
9036
|
*/
|
|
8237
9037
|
async getRedemptionTypes(options) {
|
|
8238
9038
|
const params = buildPaginationParams(options);
|
|
9039
|
+
setIncludeParam(params, options?.include);
|
|
8239
9040
|
const response = await this.apiClient.get(`${this.typesPath}?${params.toString()}`);
|
|
8240
9041
|
return normalizeToPaginated(response);
|
|
8241
9042
|
}
|
|
9043
|
+
/** PUBLIC: Get one redemption type; `translations` adds the raw per-locale map (admin editing). */
|
|
9044
|
+
async getRedemptionTypeById(id, include) {
|
|
9045
|
+
const params = include?.length ? `?include=${include.join(',')}` : '';
|
|
9046
|
+
return this.apiClient.get(`${this.typesPath}/${id}${params}`);
|
|
9047
|
+
}
|
|
8242
9048
|
/**
|
|
8243
9049
|
* PUBLIC: Get redemption by ID
|
|
8244
9050
|
*
|
|
8245
|
-
* Updated: /redemption/:id
|
|
9051
|
+
* Updated: /redemption/:id → /redemptions/:id
|
|
8246
9052
|
*
|
|
8247
9053
|
* @param id - Redemption UUID
|
|
8248
9054
|
* @param include - Relations to include: 'redeemCount'
|
|
@@ -8261,7 +9067,7 @@ class RedemptionApi {
|
|
|
8261
9067
|
/**
|
|
8262
9068
|
* PUBLIC: Get available supply for redemption
|
|
8263
9069
|
*
|
|
8264
|
-
* Updated: /redemption/:id/available-supply
|
|
9070
|
+
* Updated: /redemption/:id/available-supply → /redemptions/:id/supply
|
|
8265
9071
|
*/
|
|
8266
9072
|
async getRedemptionAvailableSupply(id) {
|
|
8267
9073
|
const result = await this.apiClient.get(`${this.basePath}/${id}/supply`);
|
|
@@ -8377,7 +9183,7 @@ class RedemptionApi {
|
|
|
8377
9183
|
/**
|
|
8378
9184
|
* ADMIN: Create redemption
|
|
8379
9185
|
*
|
|
8380
|
-
* Updated: /redemption/admin
|
|
9186
|
+
* Updated: /redemption/admin → /redemptions
|
|
8381
9187
|
*/
|
|
8382
9188
|
async createRedemption(redemption) {
|
|
8383
9189
|
return this.apiClient.post(`${this.basePath}`, redemption);
|
|
@@ -8385,7 +9191,7 @@ class RedemptionApi {
|
|
|
8385
9191
|
/**
|
|
8386
9192
|
* ADMIN: Update redemption
|
|
8387
9193
|
*
|
|
8388
|
-
* Updated: /redemption/admin/:id
|
|
9194
|
+
* Updated: /redemption/admin/:id → /redemptions/:id
|
|
8389
9195
|
*/
|
|
8390
9196
|
async updateRedemption(id, redemptionCreateRequest) {
|
|
8391
9197
|
return this.apiClient.put(`${this.basePath}/${id}`, redemptionCreateRequest);
|
|
@@ -8393,7 +9199,7 @@ class RedemptionApi {
|
|
|
8393
9199
|
/**
|
|
8394
9200
|
* UNIFIED: Toggle redemption active status
|
|
8395
9201
|
*
|
|
8396
|
-
* Updated: /redemption/admin/:id/toggle-active
|
|
9202
|
+
* Updated: /redemption/admin/:id/toggle-active → /redemptions/:id/status
|
|
8397
9203
|
* Following standard /status pattern used across domains
|
|
8398
9204
|
*/
|
|
8399
9205
|
async toggleRedemptionActive(redemptionId) {
|
|
@@ -8402,7 +9208,7 @@ class RedemptionApi {
|
|
|
8402
9208
|
/**
|
|
8403
9209
|
* ADMIN: Delete redemption
|
|
8404
9210
|
*
|
|
8405
|
-
* Updated: /redemption/admin/:id
|
|
9211
|
+
* Updated: /redemption/admin/:id → /redemptions/:id
|
|
8406
9212
|
*/
|
|
8407
9213
|
async deleteRedemption(id) {
|
|
8408
9214
|
return this.apiClient.delete(`${this.basePath}/${id}`);
|
|
@@ -8410,7 +9216,7 @@ class RedemptionApi {
|
|
|
8410
9216
|
/**
|
|
8411
9217
|
* ADMIN: Create redemption type
|
|
8412
9218
|
*
|
|
8413
|
-
* Updated: /redemption/admin/type
|
|
9219
|
+
* Updated: /redemption/admin/type → /redemption-types
|
|
8414
9220
|
*/
|
|
8415
9221
|
async createRedemptionType(redemptionType) {
|
|
8416
9222
|
return this.apiClient.post(this.typesPath, redemptionType);
|
|
@@ -8437,7 +9243,7 @@ class RedemptionApi {
|
|
|
8437
9243
|
/**
|
|
8438
9244
|
* ADMIN: Create redemption token unit
|
|
8439
9245
|
*
|
|
8440
|
-
* Updated: /redemption/admin/:id/token-units
|
|
9246
|
+
* Updated: /redemption/admin/:id/token-units → /redemption-tokens/:id/
|
|
8441
9247
|
*/
|
|
8442
9248
|
async createRedemptionTokenUnit(redemptionId, redemptionTokenUnit) {
|
|
8443
9249
|
return this.apiClient.post(`${this.basePath}/${redemptionId}/tokens`, redemptionTokenUnit);
|
|
@@ -8445,7 +9251,7 @@ class RedemptionApi {
|
|
|
8445
9251
|
/**
|
|
8446
9252
|
* ADMIN: Update redemption token unit
|
|
8447
9253
|
*
|
|
8448
|
-
* Updated: /redemption/admin/:id/token-units/:tokenUnitId
|
|
9254
|
+
* Updated: /redemption/admin/:id/token-units/:tokenUnitId → /redemption-tokens/:id/:tokenUnitId
|
|
8449
9255
|
*/
|
|
8450
9256
|
async updateRedemptionTokenUnit(redemptionId, tokenUnitId, redemptionTokenUnit) {
|
|
8451
9257
|
return this.apiClient.put(`${this.basePath}/${redemptionId}/tokens/${tokenUnitId}`, redemptionTokenUnit);
|
|
@@ -8453,7 +9259,7 @@ class RedemptionApi {
|
|
|
8453
9259
|
/**
|
|
8454
9260
|
* ADMIN: Delete redemption token unit
|
|
8455
9261
|
*
|
|
8456
|
-
* Updated: /redemption/admin/:id/token-units/:tokenUnitId
|
|
9262
|
+
* Updated: /redemption/admin/:id/token-units/:tokenUnitId → /redemption-tokens/:id/:tokenUnitId
|
|
8457
9263
|
*/
|
|
8458
9264
|
async deleteRedemptionTokenUnit(redemptionId, redemptionTokenUnitId) {
|
|
8459
9265
|
return this.apiClient.delete(`${this.basePath}/${redemptionId}/tokens/${redemptionTokenUnitId}`);
|
|
@@ -8511,6 +9317,23 @@ class RedemptionApi {
|
|
|
8511
9317
|
const url = `${this.basePath}/redeems/export/csv${queryString ? `?${queryString}` : ''}`;
|
|
8512
9318
|
return this.apiClient.get(url, 'blob');
|
|
8513
9319
|
}
|
|
9320
|
+
// ==========================================
|
|
9321
|
+
// ✅ APPROVAL WORKFLOW
|
|
9322
|
+
// ==========================================
|
|
9323
|
+
/**
|
|
9324
|
+
* ADMIN: Approve a redemption
|
|
9325
|
+
*
|
|
9326
|
+
* Approves a redemption that is pending approval, changing its status to 'approved'.
|
|
9327
|
+
*
|
|
9328
|
+
* Endpoint: PATCH /admin/redemptions/:id/approve
|
|
9329
|
+
* Auth: @TenantAdmin()
|
|
9330
|
+
*
|
|
9331
|
+
* @param redemptionId - ID of the redemption to approve
|
|
9332
|
+
* @returns Updated redemption with approval metadata
|
|
9333
|
+
*/
|
|
9334
|
+
async setRedemptionApproval(redemptionId, status, reason) {
|
|
9335
|
+
return this.apiClient.patch(`/redemptions/${redemptionId}/approval`, { status, reason });
|
|
9336
|
+
}
|
|
8514
9337
|
}
|
|
8515
9338
|
|
|
8516
9339
|
/**
|
|
@@ -8565,6 +9388,9 @@ class RedemptionService {
|
|
|
8565
9388
|
async getRedemptionTypes(options) {
|
|
8566
9389
|
return this.redemptionApi.getRedemptionTypes(options);
|
|
8567
9390
|
}
|
|
9391
|
+
async getRedemptionTypeById(id, include) {
|
|
9392
|
+
return this.redemptionApi.getRedemptionTypeById(id, include);
|
|
9393
|
+
}
|
|
8568
9394
|
// ==========================================
|
|
8569
9395
|
// AUTHENTICATED OPERATIONS
|
|
8570
9396
|
// ==========================================
|
|
@@ -9581,11 +10407,12 @@ class PurchaseApi {
|
|
|
9581
10407
|
* - ADMIN (Tenant Admin JWT): Returns filtered results based on active parameter
|
|
9582
10408
|
*
|
|
9583
10409
|
* @param active Filter by active status
|
|
9584
|
-
* @param options Pagination options (page, limit, sortBy, sortOrder)
|
|
10410
|
+
* @param options Pagination options (page, limit, sortBy, sortOrder) and `include: ['translations']` for admin editing
|
|
9585
10411
|
* @returns Paginated response with purchase tokens
|
|
9586
10412
|
*/
|
|
9587
10413
|
async getPurchaseTokens(active, options) {
|
|
9588
10414
|
const params = buildPaginationParams(options);
|
|
10415
|
+
setIncludeParam(params, options?.include);
|
|
9589
10416
|
if (active !== undefined) {
|
|
9590
10417
|
params.append('active', String(active));
|
|
9591
10418
|
}
|
|
@@ -9690,7 +10517,7 @@ class PurchaseApi {
|
|
|
9690
10517
|
*/
|
|
9691
10518
|
async getUserPurchaseHistory(options) {
|
|
9692
10519
|
const params = buildPaginationParams(options);
|
|
9693
|
-
const response = await this.apiClient.get(`${this.basePath}/me
|
|
10520
|
+
const response = await this.apiClient.get(`${this.basePath}/me?${params.toString()}`);
|
|
9694
10521
|
return normalizeToPaginated(response);
|
|
9695
10522
|
}
|
|
9696
10523
|
}
|
|
@@ -9777,10 +10604,10 @@ class TenantApi {
|
|
|
9777
10604
|
* Requires: X-API-Key header + Authorization Bearer token (Admin JWT)
|
|
9778
10605
|
* Returns: Full tenant information including allowedTokenIssuers and sensitive config
|
|
9779
10606
|
*/
|
|
9780
|
-
async getRemoteTenant() {
|
|
9781
|
-
const
|
|
9782
|
-
|
|
9783
|
-
return this.apiClient.get(
|
|
10607
|
+
async getRemoteTenant(include) {
|
|
10608
|
+
const params = new URLSearchParams({ date: Date.now().toString() });
|
|
10609
|
+
setIncludeParam(params, include);
|
|
10610
|
+
return this.apiClient.get(`${this.basePath}/me?${params.toString()}`);
|
|
9784
10611
|
}
|
|
9785
10612
|
/**
|
|
9786
10613
|
* API KEY: Get current tenant public information
|
|
@@ -9789,10 +10616,10 @@ class TenantApi {
|
|
|
9789
10616
|
* Requires: X-API-Key header (handled by PersApiClient)
|
|
9790
10617
|
* Returns: Public tenant information (no sensitive config)
|
|
9791
10618
|
*/
|
|
9792
|
-
async getRemoteTenantPublic() {
|
|
9793
|
-
const
|
|
9794
|
-
|
|
9795
|
-
return this.apiClient.get(
|
|
10619
|
+
async getRemoteTenantPublic(include) {
|
|
10620
|
+
const params = new URLSearchParams({ date: Date.now().toString() });
|
|
10621
|
+
setIncludeParam(params, include);
|
|
10622
|
+
return this.apiClient.get(`${this.basePath}/me?${params.toString()}`);
|
|
9796
10623
|
}
|
|
9797
10624
|
/**
|
|
9798
10625
|
* API KEY: Get remote login token
|
|
@@ -9817,9 +10644,11 @@ class TenantApi {
|
|
|
9817
10644
|
*
|
|
9818
10645
|
* @param tenantId - The tenant ID to retrieve public information for
|
|
9819
10646
|
*/
|
|
9820
|
-
async getPublicTenantById(tenantId) {
|
|
9821
|
-
const
|
|
9822
|
-
|
|
10647
|
+
async getPublicTenantById(tenantId, include) {
|
|
10648
|
+
const params = new URLSearchParams();
|
|
10649
|
+
setIncludeParam(params, include);
|
|
10650
|
+
const query = params.toString();
|
|
10651
|
+
return this.apiClient.get(`${this.basePath}/${tenantId}${query ? `?${query}` : ''}`);
|
|
9823
10652
|
}
|
|
9824
10653
|
// ==========================================
|
|
9825
10654
|
// ADMIN OPERATIONS
|
|
@@ -9883,14 +10712,14 @@ class TenantService {
|
|
|
9883
10712
|
/**
|
|
9884
10713
|
* Get full tenant information (Admin JWT required)
|
|
9885
10714
|
*/
|
|
9886
|
-
async getRemoteTenant() {
|
|
9887
|
-
return this.tenantApi.getRemoteTenant();
|
|
10715
|
+
async getRemoteTenant(include) {
|
|
10716
|
+
return this.tenantApi.getRemoteTenant(include);
|
|
9888
10717
|
}
|
|
9889
10718
|
/**
|
|
9890
10719
|
* PUBLIC: Get tenant public information (API key only)
|
|
9891
10720
|
*/
|
|
9892
|
-
async getRemoteTenantPublic() {
|
|
9893
|
-
return this.tenantApi.getRemoteTenantPublic();
|
|
10721
|
+
async getRemoteTenantPublic(include) {
|
|
10722
|
+
return this.tenantApi.getRemoteTenantPublic(include);
|
|
9894
10723
|
}
|
|
9895
10724
|
/**
|
|
9896
10725
|
* PUBLIC: Get remote login token
|
|
@@ -9943,6 +10772,177 @@ class TenantService {
|
|
|
9943
10772
|
}
|
|
9944
10773
|
}
|
|
9945
10774
|
|
|
10775
|
+
/**
|
|
10776
|
+
* IPFS Resolver - Gateway and URL resolution (chain-agnostic, no token or web3 types)
|
|
10777
|
+
*
|
|
10778
|
+
* @module @explorins/pers-sdk/ipfs
|
|
10779
|
+
*/
|
|
10780
|
+
const DEFAULT_IPFS_GATEWAY = 'gateway.pinata.cloud';
|
|
10781
|
+
class IpfsResolver {
|
|
10782
|
+
constructor(config) {
|
|
10783
|
+
this.config = config;
|
|
10784
|
+
this.cache = globalCacheService.createNamespace('ipfs');
|
|
10785
|
+
}
|
|
10786
|
+
/** Tenant gateway domain; falls back to a public gateway so assets still load when unconfigured. */
|
|
10787
|
+
getGatewayDomain() {
|
|
10788
|
+
return this.cache.getOrSet('gateway', async () => {
|
|
10789
|
+
const { ipfsGatewayDomain } = await this.config.getClientConfig();
|
|
10790
|
+
if (!ipfsGatewayDomain) {
|
|
10791
|
+
console.warn(`[PERS SDK] No ipfsGatewayDomain configured for the tenant, using ${DEFAULT_IPFS_GATEWAY}`);
|
|
10792
|
+
return DEFAULT_IPFS_GATEWAY;
|
|
10793
|
+
}
|
|
10794
|
+
return ipfsGatewayDomain;
|
|
10795
|
+
}, CacheTTL.GATEWAY);
|
|
10796
|
+
}
|
|
10797
|
+
/** `ipfs://` and `ar://` to HTTPS; HTTP(S) and unknown schemes pass through; empty input gives ''. */
|
|
10798
|
+
async resolveUrl(url) {
|
|
10799
|
+
if (!url)
|
|
10800
|
+
return '';
|
|
10801
|
+
if (url.startsWith('ipfs://')) {
|
|
10802
|
+
return `https://${await this.getGatewayDomain()}/ipfs/${url.slice('ipfs://'.length)}`;
|
|
10803
|
+
}
|
|
10804
|
+
if (url.startsWith('ar://')) {
|
|
10805
|
+
return `https://arweave.net/${url.slice('ar://'.length)}`;
|
|
10806
|
+
}
|
|
10807
|
+
return url;
|
|
10808
|
+
}
|
|
10809
|
+
clearCache() {
|
|
10810
|
+
this.cache.clear();
|
|
10811
|
+
}
|
|
10812
|
+
/** Fetches JSON from any supported URI; throws on a non-OK response. */
|
|
10813
|
+
async fetchJson(uri) {
|
|
10814
|
+
const resolved = await this.resolveUrl(uri);
|
|
10815
|
+
const response = await fetch(resolved);
|
|
10816
|
+
if (!response.ok) {
|
|
10817
|
+
throw new Error(`HTTP ${response.status} fetching ${resolved}`);
|
|
10818
|
+
}
|
|
10819
|
+
return response.json();
|
|
10820
|
+
}
|
|
10821
|
+
}
|
|
10822
|
+
|
|
10823
|
+
/**
|
|
10824
|
+
* Token Metadata Resolver - The single place where ERC/OpenSea metadata JSON becomes SDK TokenMetadata
|
|
10825
|
+
*
|
|
10826
|
+
* Returns raw (language-agnostic) metadata and caches it: IPFS content is immutable.
|
|
10827
|
+
* Callers apply the locale when handing the data out.
|
|
10828
|
+
*
|
|
10829
|
+
* @module @explorins/pers-sdk/token/metadata
|
|
10830
|
+
*/
|
|
10831
|
+
const STANDARD_KEYS = new Set([
|
|
10832
|
+
'name', 'description', 'image', 'image_url', 'imageUrl',
|
|
10833
|
+
'animation_url', 'animationUrl', 'external_url', 'externalUrl', 'attributes',
|
|
10834
|
+
]);
|
|
10835
|
+
const isResolvable = (value) => typeof value === 'string' && (value.startsWith('ipfs://') || value.startsWith('ar://'));
|
|
10836
|
+
const text = (...candidates) => {
|
|
10837
|
+
const found = candidates.find(value => value !== undefined && value !== null && value !== '');
|
|
10838
|
+
return found === undefined ? undefined : String(found);
|
|
10839
|
+
};
|
|
10840
|
+
class TokenMetadataResolver {
|
|
10841
|
+
constructor(ipfs) {
|
|
10842
|
+
this.ipfs = ipfs;
|
|
10843
|
+
this.cache = globalCacheService.createNamespace('token-metadata');
|
|
10844
|
+
}
|
|
10845
|
+
/** Normalized metadata with HTTPS asset URLs, or null when it cannot be fetched. */
|
|
10846
|
+
fetch(tokenUri) {
|
|
10847
|
+
return this.cache.getOrSet(`processed:${tokenUri}`, async () => {
|
|
10848
|
+
try {
|
|
10849
|
+
return await this.load(tokenUri);
|
|
10850
|
+
}
|
|
10851
|
+
catch (error) {
|
|
10852
|
+
console.warn(`[PERS SDK] Error fetching metadata from ${tokenUri}:`, error);
|
|
10853
|
+
return null;
|
|
10854
|
+
}
|
|
10855
|
+
}, CacheTTL.METADATA);
|
|
10856
|
+
}
|
|
10857
|
+
clearCache() {
|
|
10858
|
+
this.cache.clear();
|
|
10859
|
+
}
|
|
10860
|
+
async load(tokenUri) {
|
|
10861
|
+
const raw = await this.ipfs.fetchJson(tokenUri);
|
|
10862
|
+
// Resolve ipfs:// and ar:// references in any top-level string field
|
|
10863
|
+
const resolved = { ...raw };
|
|
10864
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
10865
|
+
if (isResolvable(value))
|
|
10866
|
+
resolved[key] = await this.ipfs.resolveUrl(value);
|
|
10867
|
+
}
|
|
10868
|
+
const customProperties = Object.fromEntries(Object.entries(resolved).filter(([key]) => !STANDARD_KEYS.has(key)));
|
|
10869
|
+
return {
|
|
10870
|
+
name: text(resolved.name) ?? '',
|
|
10871
|
+
description: text(resolved.description) ?? '',
|
|
10872
|
+
imageUrl: text(resolved.image, resolved.image_url, resolved.imageUrl) ?? '',
|
|
10873
|
+
animationUrl: text(resolved.animation_url, resolved.animationUrl),
|
|
10874
|
+
externalUrl: text(resolved.external_url, resolved.externalUrl),
|
|
10875
|
+
attributes: Array.isArray(resolved.attributes) ? resolved.attributes : [],
|
|
10876
|
+
...customProperties,
|
|
10877
|
+
};
|
|
10878
|
+
}
|
|
10879
|
+
}
|
|
10880
|
+
function createIpfsSources(config) {
|
|
10881
|
+
const resolver = new IpfsResolver(config);
|
|
10882
|
+
return { resolver, metadata: new TokenMetadataResolver(resolver) };
|
|
10883
|
+
}
|
|
10884
|
+
|
|
10885
|
+
/**
|
|
10886
|
+
* Pure read-time localization of token metadata resolved from IPFS/HTTP (client-side path that
|
|
10887
|
+
* bypasses the API). Caches keep the raw immutable JSON; callers apply the locale when handing data out.
|
|
10888
|
+
*
|
|
10889
|
+
* @module @explorins/pers-sdk/token/localization
|
|
10890
|
+
*/
|
|
10891
|
+
/**
|
|
10892
|
+
* Replaces `name` and `description` with the translation for `locale` from the metadata's
|
|
10893
|
+
* `translations` field (snapshotted at pin time). Falls back per field to the base text.
|
|
10894
|
+
*/
|
|
10895
|
+
function localizeTokenMetadata(metadata, locale) {
|
|
10896
|
+
if (!locale || !metadata.translations)
|
|
10897
|
+
return metadata;
|
|
10898
|
+
return { ...metadata, ...localizeFields(metadata, locale, ['name', 'description']) };
|
|
10899
|
+
}
|
|
10900
|
+
/** Same instance when the locale is off or the metadata has no translations, so memoized consumers are not re-rendered. */
|
|
10901
|
+
function localizeTokenBalance(balance, locale) {
|
|
10902
|
+
if (!locale || !balance.metadata)
|
|
10903
|
+
return balance;
|
|
10904
|
+
const metadata = localizeTokenMetadata(balance.metadata, locale);
|
|
10905
|
+
return metadata === balance.metadata ? balance : { ...balance, metadata };
|
|
10906
|
+
}
|
|
10907
|
+
function localizeTokenCollection(collection, locale) {
|
|
10908
|
+
if (!locale)
|
|
10909
|
+
return collection;
|
|
10910
|
+
const tokens = collection.tokens.map(token => localizeTokenBalance(token, locale));
|
|
10911
|
+
return tokens.every((token, i) => token === collection.tokens[i]) ? collection : { ...collection, tokens };
|
|
10912
|
+
}
|
|
10913
|
+
|
|
10914
|
+
/**
|
|
10915
|
+
* IPFS Manager - Gateway, URL and token metadata resolution (chain-agnostic, no web3 dependencies)
|
|
10916
|
+
*
|
|
10917
|
+
* @example
|
|
10918
|
+
* ```typescript
|
|
10919
|
+
* const url = await sdk.ipfs.resolveUrl('ipfs://QmXxx...');
|
|
10920
|
+
* const metadata = await sdk.ipfs.fetchMetadata('ipfs://QmXxx.../metadata.json');
|
|
10921
|
+
* ```
|
|
10922
|
+
*/
|
|
10923
|
+
class IpfsManager {
|
|
10924
|
+
constructor(config, getLocale = () => null) {
|
|
10925
|
+
this.getLocale = getLocale;
|
|
10926
|
+
this.sources = createIpfsSources(config);
|
|
10927
|
+
}
|
|
10928
|
+
/** Tenant IPFS gateway domain (cached; falls back to a public gateway when unconfigured) */
|
|
10929
|
+
getGatewayDomain() {
|
|
10930
|
+
return this.sources.resolver.getGatewayDomain();
|
|
10931
|
+
}
|
|
10932
|
+
/** Resolve `ipfs://` and `ar://` URLs to HTTPS; other URLs pass through */
|
|
10933
|
+
resolveUrl(url) {
|
|
10934
|
+
return this.sources.resolver.resolveUrl(url);
|
|
10935
|
+
}
|
|
10936
|
+
/**
|
|
10937
|
+
* Fetch token metadata from a token URI (cached; IPFS content is immutable).
|
|
10938
|
+
* name/description follow the SDK locale when the metadata carries translations.
|
|
10939
|
+
*/
|
|
10940
|
+
async fetchMetadata(tokenUri) {
|
|
10941
|
+
const metadata = await this.sources.metadata.fetch(tokenUri);
|
|
10942
|
+
return metadata ? localizeTokenMetadata(metadata, this.getLocale()) : null;
|
|
10943
|
+
}
|
|
10944
|
+
}
|
|
10945
|
+
|
|
9946
10946
|
/**
|
|
9947
10947
|
* Tenant Manager - Clean, high-level interface for tenant operations
|
|
9948
10948
|
*
|
|
@@ -9961,8 +10961,8 @@ class TenantManager {
|
|
|
9961
10961
|
*
|
|
9962
10962
|
* @returns Promise resolving to public tenant data
|
|
9963
10963
|
*/
|
|
9964
|
-
async getTenantPublicInfo() {
|
|
9965
|
-
return this.tenantService.getRemoteTenantPublic();
|
|
10964
|
+
async getTenantPublicInfo(include) {
|
|
10965
|
+
return this.tenantService.getRemoteTenantPublic(include);
|
|
9966
10966
|
}
|
|
9967
10967
|
/**
|
|
9968
10968
|
* Get full tenant information including sensitive config (Admin JWT required)
|
|
@@ -9970,10 +10970,11 @@ class TenantManager {
|
|
|
9970
10970
|
* Returns additional fields like allowedTokenIssuers that are only
|
|
9971
10971
|
* available to authenticated tenant admins.
|
|
9972
10972
|
*
|
|
10973
|
+
* @param include - Pass `['translations']` to also receive the raw per-locale translations (admin editing)
|
|
9973
10974
|
* @returns Promise resolving to full tenant data
|
|
9974
10975
|
*/
|
|
9975
|
-
async getTenantInfo() {
|
|
9976
|
-
return this.tenantService.getRemoteTenant();
|
|
10976
|
+
async getTenantInfo(include) {
|
|
10977
|
+
return this.tenantService.getRemoteTenant(include);
|
|
9977
10978
|
}
|
|
9978
10979
|
/**
|
|
9979
10980
|
* Get tenant login token
|
|
@@ -10072,99 +11073,21 @@ class TenantManager {
|
|
|
10072
11073
|
clearCache() {
|
|
10073
11074
|
this.cache.clear();
|
|
10074
11075
|
}
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
*
|
|
10081
|
-
* @returns Promise resolving to IPFS gateway domain (e.g., 'gateway.pinata.cloud')
|
|
10082
|
-
* @example
|
|
10083
|
-
* ```typescript
|
|
10084
|
-
* const gateway = await sdk.tenants.getIpfsGatewayDomain();
|
|
10085
|
-
* // Returns 'gateway.pinata.cloud' or tenant-specific gateway
|
|
10086
|
-
* ```
|
|
10087
|
-
*/
|
|
11076
|
+
/** @internal Single IpfsManager per SDK; sdk.ipfs and the web3 adapter reuse it. */
|
|
11077
|
+
get ipfs() {
|
|
11078
|
+
return this._ipfs ?? (this._ipfs = new IpfsManager(this, () => this.apiClient.getLocale()));
|
|
11079
|
+
}
|
|
11080
|
+
/** @deprecated Use `sdk.ipfs.getGatewayDomain()` */
|
|
10088
11081
|
async getIpfsGatewayDomain() {
|
|
10089
|
-
return this.
|
|
10090
|
-
const config = await this.getClientConfig();
|
|
10091
|
-
return config.ipfsGatewayDomain || 'gateway.pinata.cloud';
|
|
10092
|
-
}, CacheTTL.LONG);
|
|
11082
|
+
return this.ipfs.getGatewayDomain();
|
|
10093
11083
|
}
|
|
10094
|
-
/**
|
|
10095
|
-
* Resolve an IPFS URL to an HTTP gateway URL
|
|
10096
|
-
*
|
|
10097
|
-
* @param url - The URL to resolve (ipfs://, ar://, or HTTP URL)
|
|
10098
|
-
* @returns Promise resolving to HTTP gateway URL
|
|
10099
|
-
*
|
|
10100
|
-
* @example
|
|
10101
|
-
* ```typescript
|
|
10102
|
-
* const httpUrl = await sdk.tenants.resolveIpfsUrl('ipfs://QmXxx...');
|
|
10103
|
-
* // Returns 'https://gateway.pinata.cloud/ipfs/QmXxx...'
|
|
10104
|
-
* ```
|
|
10105
|
-
*/
|
|
11084
|
+
/** @deprecated Use `sdk.ipfs.resolveUrl()` */
|
|
10106
11085
|
async resolveIpfsUrl(url) {
|
|
10107
|
-
|
|
10108
|
-
return '';
|
|
10109
|
-
// Already an HTTP URL
|
|
10110
|
-
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
10111
|
-
return url;
|
|
10112
|
-
}
|
|
10113
|
-
const gateway = await this.getIpfsGatewayDomain();
|
|
10114
|
-
// Handle ipfs:// protocol
|
|
10115
|
-
if (url.startsWith('ipfs://')) {
|
|
10116
|
-
const cid = url.replace('ipfs://', '');
|
|
10117
|
-
return `https://${gateway}/ipfs/${cid}`;
|
|
10118
|
-
}
|
|
10119
|
-
// Handle ar:// (Arweave) protocol
|
|
10120
|
-
if (url.startsWith('ar://')) {
|
|
10121
|
-
const txId = url.replace('ar://', '');
|
|
10122
|
-
return `https://arweave.net/${txId}`;
|
|
10123
|
-
}
|
|
10124
|
-
return url;
|
|
11086
|
+
return this.ipfs.resolveUrl(url);
|
|
10125
11087
|
}
|
|
10126
|
-
/**
|
|
10127
|
-
* Fetch and process token metadata from IPFS/HTTP URI
|
|
10128
|
-
*
|
|
10129
|
-
* @param tokenUri - The token URI (ipfs://, ar://, or HTTP)
|
|
10130
|
-
* @returns Promise resolving to processed metadata or null on error
|
|
10131
|
-
*
|
|
10132
|
-
* @example
|
|
10133
|
-
* ```typescript
|
|
10134
|
-
* const metadata = await sdk.tenants.fetchAndProcessMetadata('ipfs://QmXxx.../metadata.json');
|
|
10135
|
-
* if (metadata) {
|
|
10136
|
-
* console.log(metadata.name, metadata.imageUrl);
|
|
10137
|
-
* }
|
|
10138
|
-
* ```
|
|
10139
|
-
*/
|
|
11088
|
+
/** @deprecated Use `sdk.ipfs.fetchMetadata()` */
|
|
10140
11089
|
async fetchAndProcessMetadata(tokenUri) {
|
|
10141
|
-
|
|
10142
|
-
const resolvedUri = await this.resolveIpfsUrl(tokenUri);
|
|
10143
|
-
const response = await fetch(resolvedUri);
|
|
10144
|
-
if (!response.ok) {
|
|
10145
|
-
console.warn(`Failed to fetch metadata from ${resolvedUri}: ${response.status}`);
|
|
10146
|
-
return null;
|
|
10147
|
-
}
|
|
10148
|
-
const rawMetadata = await response.json();
|
|
10149
|
-
// Normalize the metadata - resolve image URLs
|
|
10150
|
-
const imageUrl = rawMetadata.image || rawMetadata.image_url || rawMetadata.imageUrl || '';
|
|
10151
|
-
const resolvedImageUrl = await this.resolveIpfsUrl(imageUrl);
|
|
10152
|
-
const animationUrl = rawMetadata.animation_url || rawMetadata.animationUrl || '';
|
|
10153
|
-
const resolvedAnimationUrl = animationUrl ? await this.resolveIpfsUrl(animationUrl) : undefined;
|
|
10154
|
-
return {
|
|
10155
|
-
name: rawMetadata.name || 'Untitled',
|
|
10156
|
-
description: rawMetadata.description || '',
|
|
10157
|
-
imageUrl: resolvedImageUrl,
|
|
10158
|
-
animationUrl: resolvedAnimationUrl,
|
|
10159
|
-
externalUrl: rawMetadata.external_url || rawMetadata.externalUrl,
|
|
10160
|
-
attributes: rawMetadata.attributes,
|
|
10161
|
-
...rawMetadata // Include any additional properties
|
|
10162
|
-
};
|
|
10163
|
-
}
|
|
10164
|
-
catch (error) {
|
|
10165
|
-
console.warn(`Error fetching metadata from ${tokenUri}:`, error);
|
|
10166
|
-
return null;
|
|
10167
|
-
}
|
|
11090
|
+
return this.ipfs.fetchMetadata(tokenUri);
|
|
10168
11091
|
}
|
|
10169
11092
|
}
|
|
10170
11093
|
|
|
@@ -10639,6 +11562,7 @@ class TriggerSourceApi {
|
|
|
10639
11562
|
*/
|
|
10640
11563
|
async getTriggerSources(options) {
|
|
10641
11564
|
const params = buildPaginationParams(options);
|
|
11565
|
+
setIncludeParam(params, options?.include);
|
|
10642
11566
|
if (options?.type)
|
|
10643
11567
|
params.set('type', options.type);
|
|
10644
11568
|
if (options?.businessId)
|
|
@@ -10669,8 +11593,11 @@ class TriggerSourceApi {
|
|
|
10669
11593
|
* @param triggerSourceId - UUID of the trigger source
|
|
10670
11594
|
* @returns Trigger source details
|
|
10671
11595
|
*/
|
|
10672
|
-
async getTriggerSourceById(triggerSourceId) {
|
|
10673
|
-
|
|
11596
|
+
async getTriggerSourceById(triggerSourceId, include) {
|
|
11597
|
+
const params = new URLSearchParams();
|
|
11598
|
+
setIncludeParam(params, include);
|
|
11599
|
+
const query = params.toString();
|
|
11600
|
+
return this.apiClient.get(`/trigger-sources/${triggerSourceId}${query ? `?${query}` : ''}`);
|
|
10674
11601
|
}
|
|
10675
11602
|
/**
|
|
10676
11603
|
* ADMIN: Create a new trigger source
|
|
@@ -10733,8 +11660,8 @@ class TriggerSourceService {
|
|
|
10733
11660
|
/**
|
|
10734
11661
|
* Get trigger source by ID
|
|
10735
11662
|
*/
|
|
10736
|
-
async getTriggerSourceById(triggerSourceId) {
|
|
10737
|
-
return this.triggerSourceApi.getTriggerSourceById(triggerSourceId);
|
|
11663
|
+
async getTriggerSourceById(triggerSourceId, include) {
|
|
11664
|
+
return this.triggerSourceApi.getTriggerSourceById(triggerSourceId, include);
|
|
10738
11665
|
}
|
|
10739
11666
|
/**
|
|
10740
11667
|
* ADMIN: Create a new trigger source
|
|
@@ -11212,9 +12139,14 @@ class AuthService {
|
|
|
11212
12139
|
if (this.activeRefreshPromise) {
|
|
11213
12140
|
return;
|
|
11214
12141
|
}
|
|
11215
|
-
// If auth failure is already in progress,
|
|
12142
|
+
// If auth failure is already in progress, return immediately.
|
|
12143
|
+
// DO NOT await activeAuthFailurePromise here - it causes a deadlock when
|
|
12144
|
+
// handleAuthFailure() is called from within the recovery attempt itself.
|
|
12145
|
+
// Example: recovery calls loginUser() → POST /auth/token → 401 → handleAuthFailure()
|
|
12146
|
+
// Awaiting the outer promise from inside it creates a circular wait that never resolves.
|
|
12147
|
+
// The outer failure handler will complete and emit AUTH_FAILED regardless.
|
|
11216
12148
|
if (this.activeAuthFailurePromise) {
|
|
11217
|
-
return
|
|
12149
|
+
return;
|
|
11218
12150
|
}
|
|
11219
12151
|
// Create the failure promise to deduplicate concurrent calls
|
|
11220
12152
|
this.activeAuthFailurePromise = this.performAuthFailureWithRecovery();
|
|
@@ -11399,6 +12331,9 @@ class TokenRefreshManager {
|
|
|
11399
12331
|
// Retry configuration for transient failures
|
|
11400
12332
|
this.MAX_RETRY_ATTEMPTS = 3;
|
|
11401
12333
|
this.RETRY_DELAY_MS = 1000;
|
|
12334
|
+
// After a transient refresh failure, skip further attempts for a while so every request doesn't wait through the retries again
|
|
12335
|
+
this.lastRetryableFailureTime = 0;
|
|
12336
|
+
this.RETRYABLE_FAILURE_COOLDOWN_MS = 15000;
|
|
11402
12337
|
// Active refresh promise for deduplication
|
|
11403
12338
|
this.activeRefreshPromise = null;
|
|
11404
12339
|
}
|
|
@@ -11412,33 +12347,37 @@ class TokenRefreshManager {
|
|
|
11412
12347
|
if (now - this.lastValidationTime < this.validationCacheDurationMs) {
|
|
11413
12348
|
return;
|
|
11414
12349
|
}
|
|
12350
|
+
if (now - this.lastRetryableFailureTime < this.RETRYABLE_FAILURE_COOLDOWN_MS) {
|
|
12351
|
+
return;
|
|
12352
|
+
}
|
|
11415
12353
|
try {
|
|
11416
12354
|
const token = await this.authProvider.getToken();
|
|
11417
12355
|
if (!token) {
|
|
11418
|
-
|
|
11419
|
-
|
|
11420
|
-
|
|
11421
|
-
|
|
11422
|
-
if (isTokenExpired(token, marginSeconds)) {
|
|
11423
|
-
const result = await this.attemptRefreshWithRetry();
|
|
11424
|
-
if (!result.success) {
|
|
11425
|
-
if (result.retryable) {
|
|
11426
|
-
// Transient error - don't logout, let next request retry
|
|
11427
|
-
console.warn('[TokenRefreshManager] Refresh failed with retryable error, will retry on next request');
|
|
11428
|
-
return;
|
|
11429
|
-
}
|
|
11430
|
-
// Definitive auth failure - logout required
|
|
11431
|
-
await this.authService.handleAuthFailure();
|
|
11432
|
-
}
|
|
11433
|
-
else {
|
|
11434
|
-
// Refresh successful
|
|
11435
|
-
this.lastValidationTime = now;
|
|
12356
|
+
// No access token but a stored refresh token: refresh now instead of sending an anonymous request
|
|
12357
|
+
const refreshToken = await this.authProvider.getRefreshToken?.();
|
|
12358
|
+
if (!refreshToken) {
|
|
12359
|
+
return;
|
|
11436
12360
|
}
|
|
11437
12361
|
}
|
|
11438
|
-
else {
|
|
12362
|
+
else if (!isTokenExpired(token, this.calculateRefreshMargin(token))) {
|
|
11439
12363
|
// Token is valid
|
|
11440
12364
|
this.lastValidationTime = now;
|
|
12365
|
+
return;
|
|
12366
|
+
}
|
|
12367
|
+
const result = await this.attemptRefreshWithRetry();
|
|
12368
|
+
if (result.success) {
|
|
12369
|
+
this.lastValidationTime = now;
|
|
12370
|
+
this.lastRetryableFailureTime = 0;
|
|
12371
|
+
return;
|
|
11441
12372
|
}
|
|
12373
|
+
if (result.retryable) {
|
|
12374
|
+
// Transient error - don't logout, let a later request retry after the cooldown
|
|
12375
|
+
console.warn('[TokenRefreshManager] Refresh failed with retryable error, will retry after cooldown');
|
|
12376
|
+
this.lastRetryableFailureTime = Date.now();
|
|
12377
|
+
return;
|
|
12378
|
+
}
|
|
12379
|
+
// Definitive auth failure - logout required
|
|
12380
|
+
await this.authService.handleAuthFailure();
|
|
11442
12381
|
}
|
|
11443
12382
|
catch (error) {
|
|
11444
12383
|
// Unexpected error - log but don't logout (conservative approach)
|
|
@@ -11508,13 +12447,24 @@ class TokenRefreshManager {
|
|
|
11508
12447
|
if (this.activeRefreshPromise) {
|
|
11509
12448
|
return this.activeRefreshPromise;
|
|
11510
12449
|
}
|
|
12450
|
+
if (Date.now() - this.lastRetryableFailureTime < this.RETRYABLE_FAILURE_COOLDOWN_MS) {
|
|
12451
|
+
return { success: false, retryable: true };
|
|
12452
|
+
}
|
|
11511
12453
|
this.activeRefreshPromise = (async () => {
|
|
11512
12454
|
const result = await this.attemptRefreshWithRetry();
|
|
11513
|
-
if (result.success)
|
|
12455
|
+
if (result.success) {
|
|
12456
|
+
this.lastRetryableFailureTime = 0;
|
|
11514
12457
|
return result;
|
|
12458
|
+
}
|
|
11515
12459
|
// Internal failed - try provider token recovery
|
|
11516
12460
|
const recovered = await this.authService.attemptProviderTokenRecovery();
|
|
11517
|
-
|
|
12461
|
+
if (recovered) {
|
|
12462
|
+
this.lastRetryableFailureTime = 0;
|
|
12463
|
+
return { success: true, retryable: false };
|
|
12464
|
+
}
|
|
12465
|
+
if (result.retryable)
|
|
12466
|
+
this.lastRetryableFailureTime = Date.now();
|
|
12467
|
+
return result;
|
|
11518
12468
|
})();
|
|
11519
12469
|
try {
|
|
11520
12470
|
return await this.activeRefreshPromise;
|
|
@@ -11602,7 +12552,9 @@ class AuthTokenManager {
|
|
|
11602
12552
|
const val = await this.storage.get(AUTH_STORAGE_KEYS.ACCESS_TOKEN);
|
|
11603
12553
|
// Ensure we return string for tokens
|
|
11604
12554
|
const token = typeof val === 'string' ? val : null;
|
|
11605
|
-
|
|
12555
|
+
// Never cache a miss: a transient storage failure (e.g. locked iOS Keychain) would otherwise log the user out for the whole session
|
|
12556
|
+
if (token)
|
|
12557
|
+
this.cache.accessToken = token;
|
|
11606
12558
|
return token;
|
|
11607
12559
|
}
|
|
11608
12560
|
async setAccessToken(token) {
|
|
@@ -11616,7 +12568,8 @@ class AuthTokenManager {
|
|
|
11616
12568
|
}
|
|
11617
12569
|
const val = await this.storage.get(AUTH_STORAGE_KEYS.REFRESH_TOKEN);
|
|
11618
12570
|
const token = typeof val === 'string' ? val : null;
|
|
11619
|
-
|
|
12571
|
+
if (token)
|
|
12572
|
+
this.cache.refreshToken = token;
|
|
11620
12573
|
return token;
|
|
11621
12574
|
}
|
|
11622
12575
|
async setRefreshToken(token) {
|
|
@@ -11630,7 +12583,8 @@ class AuthTokenManager {
|
|
|
11630
12583
|
}
|
|
11631
12584
|
const val = await this.storage.get(AUTH_STORAGE_KEYS.PROVIDER_TOKEN);
|
|
11632
12585
|
const token = typeof val === 'string' ? val : null;
|
|
11633
|
-
|
|
12586
|
+
if (token)
|
|
12587
|
+
this.cache.providerToken = token;
|
|
11634
12588
|
return token;
|
|
11635
12589
|
}
|
|
11636
12590
|
async setProviderToken(token) {
|
|
@@ -11644,7 +12598,8 @@ class AuthTokenManager {
|
|
|
11644
12598
|
}
|
|
11645
12599
|
const authType = await this.storage.get(AUTH_STORAGE_KEYS.AUTH_TYPE);
|
|
11646
12600
|
const type = typeof authType === 'string' ? authType : null;
|
|
11647
|
-
|
|
12601
|
+
if (type)
|
|
12602
|
+
this.cache.authType = type;
|
|
11648
12603
|
return type;
|
|
11649
12604
|
}
|
|
11650
12605
|
async setAuthType(authType) {
|
|
@@ -12327,7 +13282,7 @@ class DefaultAuthProvider {
|
|
|
12327
13282
|
/** SDK package name */
|
|
12328
13283
|
const SDK_NAME = "@explorins/pers-sdk";
|
|
12329
13284
|
/** SDK version - injected from package.json at build time */
|
|
12330
|
-
const SDK_VERSION = "2.3.
|
|
13285
|
+
const SDK_VERSION = "2.3.38";
|
|
12331
13286
|
/** Full SDK identifier for headers */
|
|
12332
13287
|
const SDK_USER_AGENT = `${SDK_NAME}/${SDK_VERSION}`;
|
|
12333
13288
|
|
|
@@ -12460,6 +13415,56 @@ class TrackingManager {
|
|
|
12460
13415
|
}
|
|
12461
13416
|
}
|
|
12462
13417
|
|
|
13418
|
+
/**
|
|
13419
|
+
* Locale Manager - Content language for localized API responses
|
|
13420
|
+
*
|
|
13421
|
+
* Opt-in: no locale means no `x-locale` header and base-language strings.
|
|
13422
|
+
*
|
|
13423
|
+
* @module @explorins/pers-sdk/core/locale
|
|
13424
|
+
*/
|
|
13425
|
+
/** Device locale via `Intl`, or null where it is unavailable. */
|
|
13426
|
+
function detectDeviceLocale() {
|
|
13427
|
+
try {
|
|
13428
|
+
return normalizeLocale(Intl.DateTimeFormat().resolvedOptions().locale);
|
|
13429
|
+
}
|
|
13430
|
+
catch {
|
|
13431
|
+
return null;
|
|
13432
|
+
}
|
|
13433
|
+
}
|
|
13434
|
+
/**
|
|
13435
|
+
* Holds the configured locale and resolves it to a normalized tag.
|
|
13436
|
+
*/
|
|
13437
|
+
class LocaleManager {
|
|
13438
|
+
constructor(locale) {
|
|
13439
|
+
this.listeners = new Set();
|
|
13440
|
+
this.setting = locale;
|
|
13441
|
+
}
|
|
13442
|
+
/** Set the locale (`'auto'`, a BCP 47 tag, or `null` to disable). Listeners run when the effective locale changes. */
|
|
13443
|
+
setLocale(locale) {
|
|
13444
|
+
const before = this.getLocale();
|
|
13445
|
+
this.setting = locale;
|
|
13446
|
+
const after = this.getLocale();
|
|
13447
|
+
if (after !== before)
|
|
13448
|
+
this.listeners.forEach(listener => listener(after));
|
|
13449
|
+
}
|
|
13450
|
+
/** Subscribe to effective-locale changes (e.g. to drop locale-dependent caches). Returns an unsubscribe function. */
|
|
13451
|
+
onChange(listener) {
|
|
13452
|
+
this.listeners.add(listener);
|
|
13453
|
+
return () => this.listeners.delete(listener);
|
|
13454
|
+
}
|
|
13455
|
+
/** Normalized locale in effect, or null when disabled or invalid. */
|
|
13456
|
+
getLocale() {
|
|
13457
|
+
if (!this.setting)
|
|
13458
|
+
return null;
|
|
13459
|
+
return this.setting === 'auto' ? detectDeviceLocale() : normalizeLocale(this.setting);
|
|
13460
|
+
}
|
|
13461
|
+
/** @internal - Used by PersApiClient */
|
|
13462
|
+
buildHeaders() {
|
|
13463
|
+
const locale = this.getLocale();
|
|
13464
|
+
return locale ? { [LOCALE_HEADER]: locale } : {};
|
|
13465
|
+
}
|
|
13466
|
+
}
|
|
13467
|
+
|
|
12463
13468
|
// packages/pers-sdk/src/core/pers-api-client.ts
|
|
12464
13469
|
/**
|
|
12465
13470
|
* PERS API Client - Platform-agnostic HTTP client with authentication
|
|
@@ -12513,6 +13518,7 @@ class PersApiClient {
|
|
|
12513
13518
|
dataSource: this.mergedConfig.dataSource,
|
|
12514
13519
|
platform: this.mergedConfig.platform,
|
|
12515
13520
|
});
|
|
13521
|
+
this.localeManager = new LocaleManager(this.mergedConfig.locale);
|
|
12516
13522
|
if (this.mergedConfig.authProvider) {
|
|
12517
13523
|
this.initializationPromise = this.initialize();
|
|
12518
13524
|
}
|
|
@@ -12570,7 +13576,8 @@ class PersApiClient {
|
|
|
12570
13576
|
await this.ensureInitialized();
|
|
12571
13577
|
}
|
|
12572
13578
|
const { retryCount = 0, responseType = 'json', bypassAuth = false } = options || {};
|
|
12573
|
-
const
|
|
13579
|
+
const cacheBust = options?.cacheBust ? `${endpoint.includes('?') ? '&' : '?'}_=${Date.now()}` : '';
|
|
13580
|
+
const url = `${this.apiRoot}${endpoint}${cacheBust}`;
|
|
12574
13581
|
// TOKEN VALIDATION: Ensure token is valid before request
|
|
12575
13582
|
// This blocks the request until token validation completes - prevents 401s from expired tokens
|
|
12576
13583
|
if (!bypassAuth && this.mergedConfig.authProvider && retryCount === 0) {
|
|
@@ -12599,6 +13606,9 @@ class PersApiClient {
|
|
|
12599
13606
|
case 'PUT':
|
|
12600
13607
|
result = await this.httpClient.put(url, body, requestOptions);
|
|
12601
13608
|
break;
|
|
13609
|
+
case 'PATCH':
|
|
13610
|
+
result = await this.httpClient.patch(url, body, requestOptions);
|
|
13611
|
+
break;
|
|
12602
13612
|
case 'DELETE':
|
|
12603
13613
|
result = await this.httpClient.delete(url, requestOptions);
|
|
12604
13614
|
break;
|
|
@@ -12610,11 +13620,15 @@ class PersApiClient {
|
|
|
12610
13620
|
catch (error) {
|
|
12611
13621
|
const status = ErrorUtils.getStatus(error);
|
|
12612
13622
|
const errorMessage = ErrorUtils.getMessage(error);
|
|
13623
|
+
// A bare 304 means the platform HTTP cache did not merge it into a 200: refetch once under a unique URL
|
|
13624
|
+
if (status === 304 && method === 'GET' && !options?.cacheBust) {
|
|
13625
|
+
return await this.request(method, endpoint, body, { ...options, cacheBust: true });
|
|
13626
|
+
}
|
|
12613
13627
|
// Handle 401 Unauthorized errors
|
|
12614
13628
|
if (status === 401) {
|
|
12615
13629
|
const backendError = ErrorUtils.extractBackendErrorDetails(error);
|
|
12616
13630
|
// ===========================================
|
|
12617
|
-
// FATAL AUTH ERRORS
|
|
13631
|
+
// FATAL AUTH ERRORS → Immediate logout
|
|
12618
13632
|
// ===========================================
|
|
12619
13633
|
// Fatal codes: REFRESH_TOKEN_EXPIRED, TOKEN_REVOKED, INVALID_REFRESH_TOKEN, etc.
|
|
12620
13634
|
if (this.authService.isFatalAuthError(backendError.code)) {
|
|
@@ -12624,7 +13638,7 @@ class PersApiClient {
|
|
|
12624
13638
|
throw new AuthenticationError(errorMessage, endpoint, method, backendError);
|
|
12625
13639
|
}
|
|
12626
13640
|
// ===========================================
|
|
12627
|
-
// REFRESH REQUEST FAILED
|
|
13641
|
+
// REFRESH REQUEST FAILED → Logout (prevents infinite loop)
|
|
12628
13642
|
// ===========================================
|
|
12629
13643
|
if (options?.isRefreshRequest) {
|
|
12630
13644
|
console.warn('[PersApiClient] Refresh request itself failed with 401');
|
|
@@ -12633,12 +13647,12 @@ class PersApiClient {
|
|
|
12633
13647
|
throw new AuthenticationError(errorMessage, endpoint, method, backendError);
|
|
12634
13648
|
}
|
|
12635
13649
|
// ===========================================
|
|
12636
|
-
// RETRYABLE 401 (TOKEN_EXPIRED)
|
|
13650
|
+
// RETRYABLE 401 (TOKEN_EXPIRED) → Try refresh, then retry
|
|
12637
13651
|
// ===========================================
|
|
12638
13652
|
if (retryCount === 0 && this.mergedConfig.authProvider) {
|
|
12639
13653
|
const refreshResult = await this.refreshManager.attemptInternalRefresh();
|
|
12640
13654
|
if (refreshResult.success) {
|
|
12641
|
-
// Refresh succeeded
|
|
13655
|
+
// Refresh succeeded → retry request with new token
|
|
12642
13656
|
this._events?.emitSuccess({
|
|
12643
13657
|
type: 'token_refreshed',
|
|
12644
13658
|
domain: 'authentication',
|
|
@@ -12648,19 +13662,19 @@ class PersApiClient {
|
|
|
12648
13662
|
}
|
|
12649
13663
|
// Refresh failed - check if it's a definitive failure
|
|
12650
13664
|
if (!refreshResult.retryable) {
|
|
12651
|
-
// Non-retryable failure (e.g., refresh token expired)
|
|
13665
|
+
// Non-retryable failure (e.g., refresh token expired) → logout
|
|
12652
13666
|
console.warn('[PersApiClient] Refresh failed with non-retryable error, logging out');
|
|
12653
13667
|
await this.authService.handleAuthFailure();
|
|
12654
13668
|
}
|
|
12655
13669
|
else {
|
|
12656
|
-
// Retryable failure (network error, etc.)
|
|
13670
|
+
// Retryable failure (network error, etc.) → DON'T logout, let app retry later
|
|
12657
13671
|
console.warn('[PersApiClient] Refresh failed with retryable error, NOT logging out');
|
|
12658
13672
|
}
|
|
12659
13673
|
this.emitErrorEvent(backendError, errorMessage, endpoint, method, status, error);
|
|
12660
13674
|
throw new AuthenticationError(errorMessage, endpoint, method, backendError);
|
|
12661
13675
|
}
|
|
12662
13676
|
// ===========================================
|
|
12663
|
-
// RETRY FAILED (retryCount > 0)
|
|
13677
|
+
// RETRY FAILED (retryCount > 0) → Something wrong, but DON'T logout
|
|
12664
13678
|
// ===========================================
|
|
12665
13679
|
// We already refreshed successfully, but retry still got 401
|
|
12666
13680
|
// This is unexpected - don't logout, let app investigate
|
|
@@ -12737,6 +13751,9 @@ class PersApiClient {
|
|
|
12737
13751
|
async put(endpoint, body) {
|
|
12738
13752
|
return this.request('PUT', endpoint, body);
|
|
12739
13753
|
}
|
|
13754
|
+
async patch(endpoint, body) {
|
|
13755
|
+
return this.request('PATCH', endpoint, body);
|
|
13756
|
+
}
|
|
12740
13757
|
/**
|
|
12741
13758
|
* Generic DELETE request
|
|
12742
13759
|
*/
|
|
@@ -12799,6 +13816,8 @@ class PersApiClient {
|
|
|
12799
13816
|
}
|
|
12800
13817
|
// Add tracking headers (data source + platform)
|
|
12801
13818
|
Object.assign(headers, this.tracking.buildHeaders());
|
|
13819
|
+
// Opt-in content locale
|
|
13820
|
+
Object.assign(headers, this.localeManager.buildHeaders());
|
|
12802
13821
|
return headers;
|
|
12803
13822
|
}
|
|
12804
13823
|
// ==========================================
|
|
@@ -12822,6 +13841,15 @@ class PersApiClient {
|
|
|
12822
13841
|
getOriginalConfig() {
|
|
12823
13842
|
return this.config;
|
|
12824
13843
|
}
|
|
13844
|
+
/**
|
|
13845
|
+
* Get the resolved API root URL (e.g. `https://api.pers.ninja/v2`)
|
|
13846
|
+
*
|
|
13847
|
+
* Useful for building fully-qualified URLs outside of `apiClient.get/post/...`,
|
|
13848
|
+
* such as public redirect/QR target links.
|
|
13849
|
+
*/
|
|
13850
|
+
getApiRoot() {
|
|
13851
|
+
return this.apiRoot;
|
|
13852
|
+
}
|
|
12825
13853
|
// ==========================================
|
|
12826
13854
|
// DATA SOURCE TRACKING
|
|
12827
13855
|
// ==========================================
|
|
@@ -12849,6 +13877,21 @@ class PersApiClient {
|
|
|
12849
13877
|
validateTracking() {
|
|
12850
13878
|
return this.tracking.validate();
|
|
12851
13879
|
}
|
|
13880
|
+
// ==========================================
|
|
13881
|
+
// CONTENT LOCALE
|
|
13882
|
+
// ==========================================
|
|
13883
|
+
/** Set the content locale (`'auto'`, a BCP 47 tag, or `null` to disable) */
|
|
13884
|
+
setLocale(locale) {
|
|
13885
|
+
this.localeManager.setLocale(locale);
|
|
13886
|
+
}
|
|
13887
|
+
/** Normalized content locale in effect, or null when disabled */
|
|
13888
|
+
getLocale() {
|
|
13889
|
+
return this.localeManager.getLocale();
|
|
13890
|
+
}
|
|
13891
|
+
/** Subscribe to effective-locale changes; returns an unsubscribe function */
|
|
13892
|
+
onLocaleChange(listener) {
|
|
13893
|
+
return this.localeManager.onChange(listener);
|
|
13894
|
+
}
|
|
12852
13895
|
}
|
|
12853
13896
|
|
|
12854
13897
|
/**
|
|
@@ -13661,6 +14704,9 @@ class AuthManager {
|
|
|
13661
14704
|
* key: 'city',
|
|
13662
14705
|
* value: 'New York'
|
|
13663
14706
|
* });
|
|
14707
|
+
*
|
|
14708
|
+
* // Get a single user's public profile by ID (no auth required)
|
|
14709
|
+
* const oneProfile = await sdk.users.getPublicProfile('user-123');
|
|
13664
14710
|
* ```
|
|
13665
14711
|
*
|
|
13666
14712
|
* @example Administrative Operations
|
|
@@ -13715,7 +14761,14 @@ class UserManager {
|
|
|
13715
14761
|
* ```
|
|
13716
14762
|
*/
|
|
13717
14763
|
async getCurrentUser(options) {
|
|
13718
|
-
|
|
14764
|
+
const user = await this.userService.getRemoteUser(options);
|
|
14765
|
+
this.events?.emitSuccess({
|
|
14766
|
+
domain: 'user',
|
|
14767
|
+
type: 'user_loaded',
|
|
14768
|
+
userMessage: 'User loaded',
|
|
14769
|
+
details: { user }
|
|
14770
|
+
});
|
|
14771
|
+
return user;
|
|
13719
14772
|
}
|
|
13720
14773
|
/**
|
|
13721
14774
|
* Update current user profile
|
|
@@ -13748,7 +14801,7 @@ class UserManager {
|
|
|
13748
14801
|
const result = await this.userService.updateRemoteUser(userData);
|
|
13749
14802
|
this.events?.emitSuccess({
|
|
13750
14803
|
domain: 'user',
|
|
13751
|
-
type: '
|
|
14804
|
+
type: 'user_updated',
|
|
13752
14805
|
userMessage: 'Profile updated successfully',
|
|
13753
14806
|
details: { userId: result.id }
|
|
13754
14807
|
});
|
|
@@ -13830,6 +14883,26 @@ class UserManager {
|
|
|
13830
14883
|
async getAllUsersPublic(filter, options) {
|
|
13831
14884
|
return this.userService.getAllUsersPublicProfiles(filter || null, options);
|
|
13832
14885
|
}
|
|
14886
|
+
/**
|
|
14887
|
+
* Get a single user's public profile by ID
|
|
14888
|
+
*
|
|
14889
|
+
* Retrieves the publicly viewable profile for one user, without requiring
|
|
14890
|
+
* authentication. Useful for public profile pages/widgets where the viewer
|
|
14891
|
+
* may or may not be logged in as that user.
|
|
14892
|
+
*
|
|
14893
|
+
* @param id - The user's unique ID
|
|
14894
|
+
* @returns Promise resolving to the user's public profile
|
|
14895
|
+
* @throws {PersApiError} When no user exists with that ID
|
|
14896
|
+
*
|
|
14897
|
+
* @example Public Profile Page
|
|
14898
|
+
* ```typescript
|
|
14899
|
+
* const profile = await sdk.users.getPublicProfile(routeId);
|
|
14900
|
+
* console.log(profile.publicProfile);
|
|
14901
|
+
* ```
|
|
14902
|
+
*/
|
|
14903
|
+
async getPublicProfile(id) {
|
|
14904
|
+
return this.userService.getPublicProfileById(id);
|
|
14905
|
+
}
|
|
13833
14906
|
/**
|
|
13834
14907
|
* Admin: Get all users
|
|
13835
14908
|
*
|
|
@@ -14138,6 +15211,12 @@ class UserStatusManager {
|
|
|
14138
15211
|
async getUserStatusTypes(options) {
|
|
14139
15212
|
return this.userStatusSDK.getRemoteUserStatusTypes(options);
|
|
14140
15213
|
}
|
|
15214
|
+
/**
|
|
15215
|
+
* Get one user status type by ID; `include: ['translations']` adds the raw per-locale map for editing
|
|
15216
|
+
*/
|
|
15217
|
+
async getUserStatusTypeById(id, include) {
|
|
15218
|
+
return this.userStatusSDK.getRemoteUserStatusTypeById(id, include);
|
|
15219
|
+
}
|
|
14141
15220
|
/**
|
|
14142
15221
|
* Get earned user status for authenticated user
|
|
14143
15222
|
*
|
|
@@ -14434,6 +15513,19 @@ class TokenManager {
|
|
|
14434
15513
|
async getTokenMetadata(options) {
|
|
14435
15514
|
return this.tokenService.getTokenMetadata(options);
|
|
14436
15515
|
}
|
|
15516
|
+
/**
|
|
15517
|
+
* Get one reward or stamp metadata by ID (admin/business)
|
|
15518
|
+
*
|
|
15519
|
+
* @param include - Same relations as the list; `translations` adds the raw per-locale map for editing
|
|
15520
|
+
*
|
|
15521
|
+
* @example
|
|
15522
|
+
* ```typescript
|
|
15523
|
+
* const metadata = await sdk.tokens.getTokenMetadataById(id, ['translations']);
|
|
15524
|
+
* ```
|
|
15525
|
+
*/
|
|
15526
|
+
async getTokenMetadataById(metadataId, include) {
|
|
15527
|
+
return this.tokenService.getTokenMetadataById(metadataId, include);
|
|
15528
|
+
}
|
|
14437
15529
|
/**
|
|
14438
15530
|
* Get rewards (ERC1155 token metadata) with filtering, pagination, and include relations
|
|
14439
15531
|
*
|
|
@@ -14634,7 +15726,7 @@ class TokenManager {
|
|
|
14634
15726
|
return this.tokenService;
|
|
14635
15727
|
}
|
|
14636
15728
|
// ==========================================
|
|
14637
|
-
//
|
|
15729
|
+
// 📊 CSV EXPORT
|
|
14638
15730
|
// ==========================================
|
|
14639
15731
|
/**
|
|
14640
15732
|
* Admin: Export token metadata to CSV
|
|
@@ -14668,6 +15760,29 @@ class TokenManager {
|
|
|
14668
15760
|
async exportMetadataCSV(options) {
|
|
14669
15761
|
return this.tokenService.exportMetadataCSV(options);
|
|
14670
15762
|
}
|
|
15763
|
+
// ==========================================
|
|
15764
|
+
// ✅ APPROVAL WORKFLOW
|
|
15765
|
+
// ==========================================
|
|
15766
|
+
/**
|
|
15767
|
+
* Admin: Approve token metadata
|
|
15768
|
+
*
|
|
15769
|
+
* Approves token metadata that is pending approval. This operation is only available
|
|
15770
|
+
* when the tenant has token metadata approval enabled in their approval settings.
|
|
15771
|
+
*
|
|
15772
|
+
* @param tokenMetadataId - ID of the token metadata to approve
|
|
15773
|
+
* @returns Promise resolving to updated token metadata with approval metadata
|
|
15774
|
+
* @throws {PersApiError} When not authenticated as tenant admin or token metadata not found
|
|
15775
|
+
*
|
|
15776
|
+
* @example
|
|
15777
|
+
* ` ypescript
|
|
15778
|
+
* const approved = await sdk.tokens.approveTokenMetadata('metadata-123');
|
|
15779
|
+
* console.log('Token metadata approved at:', approved.approval?.approvedAt);
|
|
15780
|
+
* `
|
|
15781
|
+
*/
|
|
15782
|
+
async setTokenMetadataApproval(tokenMetadataId, status, reason) {
|
|
15783
|
+
const tokenApi = new TokenApi(this.apiClient);
|
|
15784
|
+
return tokenApi.setTokenMetadataApproval(tokenMetadataId, status, reason);
|
|
15785
|
+
}
|
|
14671
15786
|
}
|
|
14672
15787
|
|
|
14673
15788
|
/**
|
|
@@ -14763,6 +15878,20 @@ class BusinessManager {
|
|
|
14763
15878
|
async getActiveBusinesses(options) {
|
|
14764
15879
|
return this.businessService.getActiveBusinesses(options);
|
|
14765
15880
|
}
|
|
15881
|
+
/**
|
|
15882
|
+
* Get the distinct list of cities that have at least one business, for populating
|
|
15883
|
+
* city filter options before calling `getBusinesses({ city })`.
|
|
15884
|
+
*
|
|
15885
|
+
* @example
|
|
15886
|
+
* ```typescript
|
|
15887
|
+
* const cities = await sdk.business.getBusinessCities();
|
|
15888
|
+
* // Render as filter options, then:
|
|
15889
|
+
* const dohaBusinesses = await sdk.business.getBusinesses({ city: cities[0] });
|
|
15890
|
+
* ```
|
|
15891
|
+
*/
|
|
15892
|
+
async getBusinessCities(options) {
|
|
15893
|
+
return this.businessService.getBusinessCities(options);
|
|
15894
|
+
}
|
|
14766
15895
|
/**
|
|
14767
15896
|
* Get business by ID
|
|
14768
15897
|
*
|
|
@@ -14790,8 +15919,8 @@ class BusinessManager {
|
|
|
14790
15919
|
* }
|
|
14791
15920
|
* ```
|
|
14792
15921
|
*/
|
|
14793
|
-
async getBusinessById(businessId) {
|
|
14794
|
-
return this.businessService.getBusinessById(businessId);
|
|
15922
|
+
async getBusinessById(businessId, include) {
|
|
15923
|
+
return this.businessService.getBusinessById(businessId, include);
|
|
14795
15924
|
}
|
|
14796
15925
|
/**
|
|
14797
15926
|
* Get business by account address
|
|
@@ -14845,7 +15974,13 @@ class BusinessManager {
|
|
|
14845
15974
|
* ```
|
|
14846
15975
|
*/
|
|
14847
15976
|
async getBusinessTypes(options) {
|
|
14848
|
-
return this.businessService.getBusinessTypes();
|
|
15977
|
+
return this.businessService.getBusinessTypes(options);
|
|
15978
|
+
}
|
|
15979
|
+
/**
|
|
15980
|
+
* Get one business type by ID; `include: ['translations']` adds the raw per-locale map for editing
|
|
15981
|
+
*/
|
|
15982
|
+
async getBusinessTypeById(id, include) {
|
|
15983
|
+
return this.businessService.getBusinessTypeById(id, include);
|
|
14849
15984
|
}
|
|
14850
15985
|
/**
|
|
14851
15986
|
* Admin: Create a new business type
|
|
@@ -15004,8 +16139,8 @@ class BusinessManager {
|
|
|
15004
16139
|
* });
|
|
15005
16140
|
* ```
|
|
15006
16141
|
*/
|
|
15007
|
-
async createBusiness(
|
|
15008
|
-
const result = await this.businessService.
|
|
16142
|
+
async createBusiness(dto) {
|
|
16143
|
+
const result = await this.businessService.createBusiness(dto);
|
|
15009
16144
|
this.events?.emitSuccess({
|
|
15010
16145
|
domain: 'business',
|
|
15011
16146
|
type: 'business_created',
|
|
@@ -15310,7 +16445,7 @@ class BusinessManager {
|
|
|
15310
16445
|
return this.membershipService;
|
|
15311
16446
|
}
|
|
15312
16447
|
// ==========================================
|
|
15313
|
-
//
|
|
16448
|
+
// 📊 CSV EXPORT
|
|
15314
16449
|
// ==========================================
|
|
15315
16450
|
/**
|
|
15316
16451
|
* Admin: Export businesses to CSV
|
|
@@ -15341,6 +16476,33 @@ class BusinessManager {
|
|
|
15341
16476
|
* a.click();
|
|
15342
16477
|
* ```
|
|
15343
16478
|
*/
|
|
16479
|
+
// ==========================================
|
|
16480
|
+
// ✅ APPROVAL WORKFLOW
|
|
16481
|
+
// ==========================================
|
|
16482
|
+
/**
|
|
16483
|
+
* Admin: Approve a business
|
|
16484
|
+
*
|
|
16485
|
+
* Approves a business that is pending approval. This operation is only available
|
|
16486
|
+
* when the tenant has business approval enabled in their approval settings.
|
|
16487
|
+
*
|
|
16488
|
+
* @param businessId - ID of the business to approve
|
|
16489
|
+
* @returns Promise resolving to updated business with approval metadata
|
|
16490
|
+
* @throws {PersApiError} When not authenticated as tenant admin or business not found
|
|
16491
|
+
*
|
|
16492
|
+
* @example
|
|
16493
|
+
* ` ypescript
|
|
16494
|
+
* // Approve pending business
|
|
16495
|
+
* const approvedBusiness = await sdk.businesses.approveBusiness('business-123');
|
|
16496
|
+
*
|
|
16497
|
+
* console.log('Business approved!');
|
|
16498
|
+
* console.log('Approved by:', approvedBusiness.approval?.approvedBy);
|
|
16499
|
+
* console.log('Approved at:', approvedBusiness.approval?.approvedAt);
|
|
16500
|
+
* console.log('Status:', approvedBusiness.approval?.status); // 'approved'
|
|
16501
|
+
* `
|
|
16502
|
+
*/
|
|
16503
|
+
async setBusinessApproval(businessId, status, reason) {
|
|
16504
|
+
return this.businessApi.setBusinessApproval(businessId, status, reason);
|
|
16505
|
+
}
|
|
15344
16506
|
async exportCSV(options) {
|
|
15345
16507
|
return this.businessApi.exportCSV(options);
|
|
15346
16508
|
}
|
|
@@ -15557,7 +16719,7 @@ class CampaignManager {
|
|
|
15557
16719
|
* if (claim.rewards?.length) {
|
|
15558
16720
|
* console.log(` Rewards:`);
|
|
15559
16721
|
* claim.rewards.forEach(reward => {
|
|
15560
|
-
* console.log(`
|
|
16722
|
+
* console.log(` • ${reward.amount} ${reward.token.symbol}`);
|
|
15561
16723
|
* });
|
|
15562
16724
|
* }
|
|
15563
16725
|
* });
|
|
@@ -16368,6 +17530,29 @@ class CampaignManager {
|
|
|
16368
17530
|
getCampaignService() {
|
|
16369
17531
|
return this.campaignService;
|
|
16370
17532
|
}
|
|
17533
|
+
// ==========================================
|
|
17534
|
+
// ✅ APPROVAL WORKFLOW
|
|
17535
|
+
// ==========================================
|
|
17536
|
+
/**
|
|
17537
|
+
* Admin: Approve a campaign
|
|
17538
|
+
*
|
|
17539
|
+
* Approves a campaign that is pending approval. This operation is only available
|
|
17540
|
+
* when the tenant has campaign approval enabled in their approval settings.
|
|
17541
|
+
*
|
|
17542
|
+
* @param campaignId - ID of the campaign to approve
|
|
17543
|
+
* @returns Promise resolving to updated campaign with approval metadata
|
|
17544
|
+
* @throws {PersApiError} When not authenticated as tenant admin or campaign not found
|
|
17545
|
+
*
|
|
17546
|
+
* @example
|
|
17547
|
+
* ` ypescript
|
|
17548
|
+
* const approvedCampaign = await sdk.campaigns.approveCampaign('campaign-123');
|
|
17549
|
+
* console.log('Campaign approved at:', approvedCampaign.approval?.approvedAt);
|
|
17550
|
+
* `
|
|
17551
|
+
*/
|
|
17552
|
+
async setCampaignApproval(campaignId, status, reason) {
|
|
17553
|
+
const campaignApi = new CampaignApi(this.apiClient);
|
|
17554
|
+
return campaignApi.setCampaignApproval(campaignId, status, reason);
|
|
17555
|
+
}
|
|
16371
17556
|
}
|
|
16372
17557
|
|
|
16373
17558
|
/**
|
|
@@ -16633,6 +17818,12 @@ class RedemptionManager {
|
|
|
16633
17818
|
async getRedemptionTypes(options) {
|
|
16634
17819
|
return this.redemptionService.getRedemptionTypes(options);
|
|
16635
17820
|
}
|
|
17821
|
+
/**
|
|
17822
|
+
* Get one redemption type by ID; `include: ['translations']` adds the raw per-locale map for editing
|
|
17823
|
+
*/
|
|
17824
|
+
async getRedemptionTypeById(id, include) {
|
|
17825
|
+
return this.redemptionService.getRedemptionTypeById(id, include);
|
|
17826
|
+
}
|
|
16636
17827
|
/**
|
|
16637
17828
|
* Redeem a redemption offer
|
|
16638
17829
|
*
|
|
@@ -16645,8 +17836,9 @@ class RedemptionManager {
|
|
|
16645
17836
|
* issues ERC721 tokens. Context can include validity dates, AI prompt placeholders,
|
|
16646
17837
|
* and custom metadata.
|
|
16647
17838
|
*
|
|
16648
|
-
* @param
|
|
16649
|
-
*
|
|
17839
|
+
* @param request - Redemption ID string, or a full `RedemptionRedeemRequestDTO` object
|
|
17840
|
+
* (supports `redemptionId`, `businessId`, and `context` fields)
|
|
17841
|
+
* @param context - Optional dynamic context when passing a plain `redemptionId` string
|
|
16650
17842
|
* @returns Promise resolving to redemption result with confirmation details
|
|
16651
17843
|
* @throws {PersApiError} When redemption is not available or user lacks required tokens
|
|
16652
17844
|
*
|
|
@@ -16741,16 +17933,16 @@ class RedemptionManager {
|
|
|
16741
17933
|
* }
|
|
16742
17934
|
* ```
|
|
16743
17935
|
*/
|
|
16744
|
-
async redeem(
|
|
16745
|
-
const
|
|
16746
|
-
? { redemptionId, context }
|
|
16747
|
-
:
|
|
16748
|
-
const result = await this.redemptionService.redeemRedemption(
|
|
17936
|
+
async redeem(request, context) {
|
|
17937
|
+
const requestBody = typeof request === 'string'
|
|
17938
|
+
? { redemptionId: request, ...(context ? { context } : {}) }
|
|
17939
|
+
: request;
|
|
17940
|
+
const result = await this.redemptionService.redeemRedemption(requestBody);
|
|
16749
17941
|
this.events?.emitSuccess({
|
|
16750
17942
|
domain: 'redemption',
|
|
16751
17943
|
type: 'redeem_success',
|
|
16752
17944
|
userMessage: 'Reward redeemed successfully',
|
|
16753
|
-
details: { redemptionId, hasContext: !!context }
|
|
17945
|
+
details: { redemptionId: requestBody.redemptionId, hasContext: !!(requestBody.context ?? context) }
|
|
16754
17946
|
});
|
|
16755
17947
|
return result;
|
|
16756
17948
|
}
|
|
@@ -17231,6 +18423,29 @@ class RedemptionManager {
|
|
|
17231
18423
|
getRedemptionService() {
|
|
17232
18424
|
return this.redemptionService;
|
|
17233
18425
|
}
|
|
18426
|
+
// ==========================================
|
|
18427
|
+
// ✅ APPROVAL WORKFLOW
|
|
18428
|
+
// ==========================================
|
|
18429
|
+
/**
|
|
18430
|
+
* Admin: Approve a redemption
|
|
18431
|
+
*
|
|
18432
|
+
* Approves a redemption that is pending approval. This operation is only available
|
|
18433
|
+
* when the tenant has redemption approval enabled in their approval settings.
|
|
18434
|
+
*
|
|
18435
|
+
* @param redemptionId - ID of the redemption to approve
|
|
18436
|
+
* @returns Promise resolving to updated redemption with approval metadata
|
|
18437
|
+
* @throws {PersApiError} When not authenticated as tenant admin or redemption not found
|
|
18438
|
+
*
|
|
18439
|
+
* @example
|
|
18440
|
+
* ` ypescript
|
|
18441
|
+
* const approved = await sdk.redemptions.approveRedemption('redemption-123');
|
|
18442
|
+
* console.log('Redemption approved at:', approved.approval?.approvedAt);
|
|
18443
|
+
* `
|
|
18444
|
+
*/
|
|
18445
|
+
async setRedemptionApproval(redemptionId, status, reason) {
|
|
18446
|
+
const redemptionApi = new RedemptionApi(this.apiClient);
|
|
18447
|
+
return redemptionApi.setRedemptionApproval(redemptionId, status, reason);
|
|
18448
|
+
}
|
|
17234
18449
|
}
|
|
17235
18450
|
|
|
17236
18451
|
/**
|
|
@@ -18826,7 +20041,7 @@ class ApiKeyManager {
|
|
|
18826
20041
|
*/
|
|
18827
20042
|
async listApiKeys(type, includeRevoked = false, options) {
|
|
18828
20043
|
try {
|
|
18829
|
-
return await this.apiKeyApi.getTenantApiKeys(type, includeRevoked);
|
|
20044
|
+
return await this.apiKeyApi.getTenantApiKeys(type, includeRevoked, options);
|
|
18830
20045
|
}
|
|
18831
20046
|
catch (error) {
|
|
18832
20047
|
console.error('[ApiKeyManager] Failed to list API keys:', error);
|
|
@@ -19585,8 +20800,8 @@ class TriggerSourceManager {
|
|
|
19585
20800
|
* }
|
|
19586
20801
|
* ```
|
|
19587
20802
|
*/
|
|
19588
|
-
async getById(triggerSourceId) {
|
|
19589
|
-
return this.triggerSourceService.getTriggerSourceById(triggerSourceId);
|
|
20803
|
+
async getById(triggerSourceId, include) {
|
|
20804
|
+
return this.triggerSourceService.getTriggerSourceById(triggerSourceId, include);
|
|
19590
20805
|
}
|
|
19591
20806
|
/**
|
|
19592
20807
|
* Admin: Create a new trigger source
|
|
@@ -19707,6 +20922,207 @@ class TriggerSourceManager {
|
|
|
19707
20922
|
}
|
|
19708
20923
|
}
|
|
19709
20924
|
|
|
20925
|
+
/**
|
|
20926
|
+
* Platform-Agnostic Redirect API Client
|
|
20927
|
+
*
|
|
20928
|
+
* Handles QR code / short-link redirect operations:
|
|
20929
|
+
* - CRUD for tenant/business-owned redirects
|
|
20930
|
+
* - Per-redirect scan analytics
|
|
20931
|
+
*
|
|
20932
|
+
* Uses @explorins/pers-shared DTOs for consistency with backend.
|
|
20933
|
+
*/
|
|
20934
|
+
class RedirectApi {
|
|
20935
|
+
constructor(apiClient) {
|
|
20936
|
+
this.apiClient = apiClient;
|
|
20937
|
+
}
|
|
20938
|
+
/**
|
|
20939
|
+
* ADMIN: Create a new redirect
|
|
20940
|
+
*
|
|
20941
|
+
* @example
|
|
20942
|
+
* ```typescript
|
|
20943
|
+
* const redirect = await redirectApi.createRedirect({
|
|
20944
|
+
* targetUrl: 'https://app.pers.ninja/campaigns/abc',
|
|
20945
|
+
* label: 'Summer Campaign QR'
|
|
20946
|
+
* });
|
|
20947
|
+
* ```
|
|
20948
|
+
*/
|
|
20949
|
+
async createRedirect(redirect) {
|
|
20950
|
+
return this.apiClient.post('/redirects', redirect);
|
|
20951
|
+
}
|
|
20952
|
+
/**
|
|
20953
|
+
* ADMIN: List all redirects for the current tenant
|
|
20954
|
+
*/
|
|
20955
|
+
async getRedirects() {
|
|
20956
|
+
return this.apiClient.get('/redirects');
|
|
20957
|
+
}
|
|
20958
|
+
/**
|
|
20959
|
+
* ADMIN: Get a redirect by ID
|
|
20960
|
+
*/
|
|
20961
|
+
async getRedirectById(redirectId) {
|
|
20962
|
+
return this.apiClient.get(`/redirects/${redirectId}`);
|
|
20963
|
+
}
|
|
20964
|
+
/**
|
|
20965
|
+
* ADMIN: Update a redirect's label or target URL
|
|
20966
|
+
*/
|
|
20967
|
+
async updateRedirect(redirectId, redirect) {
|
|
20968
|
+
return this.apiClient.patch(`/redirects/${redirectId}`, redirect);
|
|
20969
|
+
}
|
|
20970
|
+
/**
|
|
20971
|
+
* ADMIN: Soft-delete a redirect
|
|
20972
|
+
*/
|
|
20973
|
+
async deleteRedirect(redirectId) {
|
|
20974
|
+
await this.apiClient.delete(`/redirects/${redirectId}`);
|
|
20975
|
+
}
|
|
20976
|
+
/**
|
|
20977
|
+
* TENANT ONLY: Get scan analytics for a redirect
|
|
20978
|
+
*
|
|
20979
|
+
* @example
|
|
20980
|
+
* ```typescript
|
|
20981
|
+
* const stats = await redirectApi.getRedirectAnalytics(redirectId, { from: '2025-01-01' });
|
|
20982
|
+
* ```
|
|
20983
|
+
*/
|
|
20984
|
+
async getRedirectAnalytics(redirectId, options) {
|
|
20985
|
+
const params = new URLSearchParams();
|
|
20986
|
+
if (options?.from)
|
|
20987
|
+
params.set('from', this.toDateParam(options.from));
|
|
20988
|
+
if (options?.to)
|
|
20989
|
+
params.set('to', this.toDateParam(options.to));
|
|
20990
|
+
const query = params.toString();
|
|
20991
|
+
return this.apiClient.get(`/redirects/${redirectId}/analytics${query ? `?${query}` : ''}`);
|
|
20992
|
+
}
|
|
20993
|
+
toDateParam(value) {
|
|
20994
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
20995
|
+
}
|
|
20996
|
+
}
|
|
20997
|
+
|
|
20998
|
+
/**
|
|
20999
|
+
* Platform-Agnostic Redirect Service
|
|
21000
|
+
*
|
|
21001
|
+
* Contains redirect business logic and operations that work across platforms.
|
|
21002
|
+
* No framework dependencies - pure TypeScript business logic.
|
|
21003
|
+
*/
|
|
21004
|
+
class RedirectService {
|
|
21005
|
+
constructor(redirectApi) {
|
|
21006
|
+
this.redirectApi = redirectApi;
|
|
21007
|
+
}
|
|
21008
|
+
/** ADMIN: Create a new redirect */
|
|
21009
|
+
async createRedirect(redirect) {
|
|
21010
|
+
return this.redirectApi.createRedirect(redirect);
|
|
21011
|
+
}
|
|
21012
|
+
/** ADMIN: List all redirects for the current tenant */
|
|
21013
|
+
async getRedirects() {
|
|
21014
|
+
return this.redirectApi.getRedirects();
|
|
21015
|
+
}
|
|
21016
|
+
/** ADMIN: Get a redirect by ID */
|
|
21017
|
+
async getRedirectById(redirectId) {
|
|
21018
|
+
return this.redirectApi.getRedirectById(redirectId);
|
|
21019
|
+
}
|
|
21020
|
+
/** ADMIN: Update a redirect's label or target URL */
|
|
21021
|
+
async updateRedirect(redirectId, redirect) {
|
|
21022
|
+
return this.redirectApi.updateRedirect(redirectId, redirect);
|
|
21023
|
+
}
|
|
21024
|
+
/** ADMIN: Soft-delete a redirect */
|
|
21025
|
+
async deleteRedirect(redirectId) {
|
|
21026
|
+
return this.redirectApi.deleteRedirect(redirectId);
|
|
21027
|
+
}
|
|
21028
|
+
/** TENANT ONLY: Get scan analytics for a redirect */
|
|
21029
|
+
async getRedirectAnalytics(redirectId, options) {
|
|
21030
|
+
return this.redirectApi.getRedirectAnalytics(redirectId, options);
|
|
21031
|
+
}
|
|
21032
|
+
}
|
|
21033
|
+
|
|
21034
|
+
/**
|
|
21035
|
+
* Redirect Manager - Clean, high-level interface for QR code / short-link tracking
|
|
21036
|
+
*
|
|
21037
|
+
* Manages redirects: short-coded links (e.g. printed on QR codes) that resolve
|
|
21038
|
+
* to a target URL and record per-scan analytics (device type, day, channel).
|
|
21039
|
+
*
|
|
21040
|
+
* @group Managers
|
|
21041
|
+
* @category Redirect Management
|
|
21042
|
+
*
|
|
21043
|
+
* @example Basic Redirect Operations
|
|
21044
|
+
* ```typescript
|
|
21045
|
+
* // Create a redirect
|
|
21046
|
+
* const redirect = await sdk.redirects.create({
|
|
21047
|
+
* targetUrl: 'https://app.pers.ninja/campaigns/abc',
|
|
21048
|
+
* label: 'Summer Campaign QR'
|
|
21049
|
+
* });
|
|
21050
|
+
*
|
|
21051
|
+
* // Build the QR target URL
|
|
21052
|
+
* const qrUrl = sdk.redirects.getQrUrl(redirect.code);
|
|
21053
|
+
*
|
|
21054
|
+
* // Get scan analytics
|
|
21055
|
+
* const stats = await sdk.redirects.getAnalytics(redirect.id, { from: '2025-01-01' });
|
|
21056
|
+
* ```
|
|
21057
|
+
*/
|
|
21058
|
+
class RedirectManager {
|
|
21059
|
+
constructor(apiClient, events) {
|
|
21060
|
+
this.apiClient = apiClient;
|
|
21061
|
+
this.events = events;
|
|
21062
|
+
const redirectApi = new RedirectApi(apiClient);
|
|
21063
|
+
this.redirectService = new RedirectService(redirectApi);
|
|
21064
|
+
}
|
|
21065
|
+
/**
|
|
21066
|
+
* ADMIN: Create a new redirect
|
|
21067
|
+
*
|
|
21068
|
+
* @example
|
|
21069
|
+
* ```typescript
|
|
21070
|
+
* const redirect = await sdk.redirects.create({ targetUrl, label: 'Summer QR' });
|
|
21071
|
+
* ```
|
|
21072
|
+
*/
|
|
21073
|
+
async create(redirect) {
|
|
21074
|
+
return this.redirectService.createRedirect(redirect);
|
|
21075
|
+
}
|
|
21076
|
+
/**
|
|
21077
|
+
* ADMIN: Get all redirects for the current tenant
|
|
21078
|
+
*/
|
|
21079
|
+
async getAll() {
|
|
21080
|
+
return this.redirectService.getRedirects();
|
|
21081
|
+
}
|
|
21082
|
+
/**
|
|
21083
|
+
* ADMIN: Get a redirect by ID
|
|
21084
|
+
*/
|
|
21085
|
+
async getById(redirectId) {
|
|
21086
|
+
return this.redirectService.getRedirectById(redirectId);
|
|
21087
|
+
}
|
|
21088
|
+
/**
|
|
21089
|
+
* ADMIN: Update a redirect's label or target URL
|
|
21090
|
+
*/
|
|
21091
|
+
async update(redirectId, redirect) {
|
|
21092
|
+
return this.redirectService.updateRedirect(redirectId, redirect);
|
|
21093
|
+
}
|
|
21094
|
+
/**
|
|
21095
|
+
* ADMIN: Soft-delete a redirect
|
|
21096
|
+
*/
|
|
21097
|
+
async delete(redirectId) {
|
|
21098
|
+
return this.redirectService.deleteRedirect(redirectId);
|
|
21099
|
+
}
|
|
21100
|
+
/**
|
|
21101
|
+
* TENANT ONLY: Get scan analytics for a redirect
|
|
21102
|
+
*
|
|
21103
|
+
* @param redirectId - UUID of the redirect
|
|
21104
|
+
* @param options - Optional `from`/`to` date range filters
|
|
21105
|
+
*
|
|
21106
|
+
* @example
|
|
21107
|
+
* ```typescript
|
|
21108
|
+
* const stats = await sdk.redirects.getAnalytics(redirect.id, { from: '2025-01-01' });
|
|
21109
|
+
* console.log(stats.total, stats.byDevice);
|
|
21110
|
+
* ```
|
|
21111
|
+
*/
|
|
21112
|
+
async getAnalytics(redirectId, options) {
|
|
21113
|
+
return this.redirectService.getRedirectAnalytics(redirectId, options);
|
|
21114
|
+
}
|
|
21115
|
+
/**
|
|
21116
|
+
* Build the public QR target URL for a redirect's code.
|
|
21117
|
+
*
|
|
21118
|
+
* The public endpoint (`GET /redirects/go/:code`) requires no auth and issues
|
|
21119
|
+
* a 302 to the redirect's `targetUrl`.
|
|
21120
|
+
*/
|
|
21121
|
+
getQrUrl(code) {
|
|
21122
|
+
return `${this.apiClient.getApiRoot()}/redirects/go/${code}`;
|
|
21123
|
+
}
|
|
21124
|
+
}
|
|
21125
|
+
|
|
19710
21126
|
/**
|
|
19711
21127
|
* Webhook API - Low-level API client for webhook operations
|
|
19712
21128
|
*
|
|
@@ -20386,7 +21802,7 @@ class WebhookManager {
|
|
|
20386
21802
|
*/
|
|
20387
21803
|
const DEFAULT_CONFIG = {
|
|
20388
21804
|
autoReconnect: true,
|
|
20389
|
-
maxReconnectAttempts:
|
|
21805
|
+
maxReconnectAttempts: Infinity,
|
|
20390
21806
|
reconnectDelay: 1000,
|
|
20391
21807
|
connectionTimeout: 30000,
|
|
20392
21808
|
debug: false,
|
|
@@ -20402,8 +21818,13 @@ class PersEventsClient {
|
|
|
20402
21818
|
// Event handlers by type
|
|
20403
21819
|
this.handlers = new Map();
|
|
20404
21820
|
this.stateHandlers = new Set();
|
|
21821
|
+
this.notificationHandlers = new Set();
|
|
20405
21822
|
// Connection info from server
|
|
20406
21823
|
this.connectionInfo = null;
|
|
21824
|
+
// Notification routing context (channelId + topicIds), resolved server-side by the relay
|
|
21825
|
+
this.notificationContext = { notificationChannelId: null, topicIds: [] };
|
|
21826
|
+
// Explicit channelId to (re-)bind on next connect, set via bindNotificationChannel() before connect resolves
|
|
21827
|
+
this.pendingNotificationChannelId = null;
|
|
20407
21828
|
// Current subscription state
|
|
20408
21829
|
this.subscriptionState = { wallets: [], chains: [], activeChains: [] };
|
|
20409
21830
|
// Pending subscription promise resolver
|
|
@@ -20430,6 +21851,9 @@ class PersEventsClient {
|
|
|
20430
21851
|
this.cleanup();
|
|
20431
21852
|
this.setState('error');
|
|
20432
21853
|
reject(new Error(`Connection timeout after ${this.config.connectionTimeout}ms`));
|
|
21854
|
+
// Trigger auto-reconnect: cleanup() nullified ws.onclose so it won't fire.
|
|
21855
|
+
// Without this call, a timed-out initial connection silently dies with no retry.
|
|
21856
|
+
this.handleDisconnect();
|
|
20433
21857
|
}, this.config.connectionTimeout);
|
|
20434
21858
|
const clearTimeoutAndResolve = () => {
|
|
20435
21859
|
clearTimeout(connectionTimeout);
|
|
@@ -20530,6 +21954,52 @@ class PersEventsClient {
|
|
|
20530
21954
|
return { ...this.subscriptionState };
|
|
20531
21955
|
}
|
|
20532
21956
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
21957
|
+
// Notifications (Flow 1 — personal channel real-time wake signal)
|
|
21958
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21959
|
+
/**
|
|
21960
|
+
* Bind this socket to a personal notification channelId (from `GET /notifications/channel-id`).
|
|
21961
|
+
*
|
|
21962
|
+
* The relay auto-resolves the channelId server-side on connect, so this is an optimization
|
|
21963
|
+
* for immediate binding rather than a requirement — safe to call any time after connect().
|
|
21964
|
+
* If not yet connected, the channelId is sent as soon as the connection opens.
|
|
21965
|
+
*/
|
|
21966
|
+
bindNotificationChannel(channelId, topicIds) {
|
|
21967
|
+
this.pendingNotificationChannelId = channelId;
|
|
21968
|
+
if (this.state !== 'connected' || !this.ws)
|
|
21969
|
+
return;
|
|
21970
|
+
const message = { type: 'subscribe', notificationChannelId: channelId, ...(topicIds ? { topicIds } : {}) };
|
|
21971
|
+
this.ws.send(JSON.stringify(message));
|
|
21972
|
+
this.log('Sent notification channel bind:', message);
|
|
21973
|
+
}
|
|
21974
|
+
/**
|
|
21975
|
+
* Rebind the personal notification channelId on an already-connected socket
|
|
21976
|
+
* (e.g. after re-auth resolves a new channelId).
|
|
21977
|
+
*/
|
|
21978
|
+
updateNotificationChannel(channelId) {
|
|
21979
|
+
this.pendingNotificationChannelId = channelId;
|
|
21980
|
+
if (this.state !== 'connected' || !this.ws)
|
|
21981
|
+
return;
|
|
21982
|
+
const message = { type: 'subscription.updated', notificationChannelId: channelId };
|
|
21983
|
+
this.ws.send(JSON.stringify(message));
|
|
21984
|
+
this.log('Sent notification channel update:', message);
|
|
21985
|
+
}
|
|
21986
|
+
/**
|
|
21987
|
+
* Get the notification routing context (channelId + topicIds) resolved for this socket.
|
|
21988
|
+
*/
|
|
21989
|
+
getNotificationContext() {
|
|
21990
|
+
return { ...this.notificationContext };
|
|
21991
|
+
}
|
|
21992
|
+
/**
|
|
21993
|
+
* Subscribe to real-time notification wake signals.
|
|
21994
|
+
*
|
|
21995
|
+
* The signal carries no notification content — always follow up with
|
|
21996
|
+
* `sdk.notifications.list()` / `getUnreadCount()`.
|
|
21997
|
+
*/
|
|
21998
|
+
onNotificationPending(handler) {
|
|
21999
|
+
this.notificationHandlers.add(handler);
|
|
22000
|
+
return () => { this.notificationHandlers.delete(handler); };
|
|
22001
|
+
}
|
|
22002
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20533
22003
|
// Event Handlers
|
|
20534
22004
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
20535
22005
|
/**
|
|
@@ -20623,15 +22093,29 @@ class PersEventsClient {
|
|
|
20623
22093
|
switch (message.type) {
|
|
20624
22094
|
case 'connected':
|
|
20625
22095
|
this.connectionInfo = message.payload;
|
|
22096
|
+
this.notificationContext = {
|
|
22097
|
+
notificationChannelId: message.payload.notificationChannelId ?? null,
|
|
22098
|
+
topicIds: message.payload.topicIds ?? [],
|
|
22099
|
+
};
|
|
20626
22100
|
this.reconnectAttempts = 0;
|
|
20627
22101
|
this.setState('connected');
|
|
20628
22102
|
this.log('Connected:', message.payload);
|
|
20629
22103
|
onConnected?.();
|
|
20630
22104
|
// Restore subscriptions on reconnect
|
|
20631
22105
|
this.restoreSubscriptions();
|
|
22106
|
+
// Re-bind an explicitly-set notification channel (overrides the relay's auto-resolved one)
|
|
22107
|
+
if (this.pendingNotificationChannelId) {
|
|
22108
|
+
this.bindNotificationChannel(this.pendingNotificationChannelId);
|
|
22109
|
+
}
|
|
20632
22110
|
break;
|
|
20633
22111
|
case 'subscribed':
|
|
20634
22112
|
this.subscriptionState = message.payload;
|
|
22113
|
+
if (message.payload.notificationChannelId !== undefined || message.payload.topicIds !== undefined) {
|
|
22114
|
+
this.notificationContext = {
|
|
22115
|
+
notificationChannelId: message.payload.notificationChannelId ?? this.notificationContext.notificationChannelId,
|
|
22116
|
+
topicIds: message.payload.topicIds ?? this.notificationContext.topicIds,
|
|
22117
|
+
};
|
|
22118
|
+
}
|
|
20635
22119
|
this.log('Subscription confirmed:', message.payload);
|
|
20636
22120
|
this.pendingSubscription?.resolve();
|
|
20637
22121
|
break;
|
|
@@ -20643,6 +22127,9 @@ class PersEventsClient {
|
|
|
20643
22127
|
case 'event':
|
|
20644
22128
|
this.routeEvent(message.payload);
|
|
20645
22129
|
break;
|
|
22130
|
+
case 'notification.pending':
|
|
22131
|
+
this.routeNotification(message.payload);
|
|
22132
|
+
break;
|
|
20646
22133
|
case 'ping':
|
|
20647
22134
|
// Respond to server ping with pong
|
|
20648
22135
|
this.sendPong();
|
|
@@ -20708,6 +22195,17 @@ class PersEventsClient {
|
|
|
20708
22195
|
}
|
|
20709
22196
|
});
|
|
20710
22197
|
}
|
|
22198
|
+
routeNotification(info) {
|
|
22199
|
+
this.log('Notification pending:', info);
|
|
22200
|
+
this.notificationHandlers.forEach(handler => {
|
|
22201
|
+
try {
|
|
22202
|
+
handler(info);
|
|
22203
|
+
}
|
|
22204
|
+
catch (err) {
|
|
22205
|
+
console.error('[PERS-Events] Notification handler error:', err);
|
|
22206
|
+
}
|
|
22207
|
+
});
|
|
22208
|
+
}
|
|
20711
22209
|
handleDisconnect() {
|
|
20712
22210
|
this.cleanup();
|
|
20713
22211
|
this.setState('disconnected');
|
|
@@ -20716,8 +22214,8 @@ class PersEventsClient {
|
|
|
20716
22214
|
this.attemptReconnect();
|
|
20717
22215
|
}
|
|
20718
22216
|
else {
|
|
22217
|
+
// maxReconnectAttempts exceeded (only reachable if explicitly set to a finite number)
|
|
20719
22218
|
this.log(`Max reconnect attempts (${this.config.maxReconnectAttempts}) reached. Call connect() manually to retry.`);
|
|
20720
|
-
// Reset counter so manual connect() will work
|
|
20721
22219
|
this.reconnectAttempts = 0;
|
|
20722
22220
|
}
|
|
20723
22221
|
}
|
|
@@ -20726,7 +22224,10 @@ class PersEventsClient {
|
|
|
20726
22224
|
const delay = Math.min(this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts), 30000 // Max 30 second delay
|
|
20727
22225
|
);
|
|
20728
22226
|
this.reconnectAttempts++;
|
|
20729
|
-
|
|
22227
|
+
const attemptLabel = isFinite(this.config.maxReconnectAttempts)
|
|
22228
|
+
? `${this.reconnectAttempts}/${this.config.maxReconnectAttempts}`
|
|
22229
|
+
: `${this.reconnectAttempts}`;
|
|
22230
|
+
this.log(`🔄 Reconnecting in ${delay}ms (attempt ${attemptLabel})`);
|
|
20730
22231
|
this.setState('reconnecting');
|
|
20731
22232
|
this.reconnectTimeout = setTimeout(async () => {
|
|
20732
22233
|
try {
|
|
@@ -20836,6 +22337,8 @@ class WalletEventsManager {
|
|
|
20836
22337
|
this.eventEmitter = eventEmitter;
|
|
20837
22338
|
this.client = null;
|
|
20838
22339
|
this.pendingHandlers = [];
|
|
22340
|
+
this.pendingNotificationHandlers = [];
|
|
22341
|
+
this.pendingNotificationChannelId = null;
|
|
20839
22342
|
this.unsubscribes = [];
|
|
20840
22343
|
// Track if WS gave up reconnecting (so we can retry on token refresh)
|
|
20841
22344
|
this.reconnectGaveUp = false;
|
|
@@ -20864,8 +22367,9 @@ class WalletEventsManager {
|
|
|
20864
22367
|
* For auto-subscription, use `sdk.connectWalletEvents()` instead.
|
|
20865
22368
|
*/
|
|
20866
22369
|
async connect() {
|
|
20867
|
-
//
|
|
20868
|
-
|
|
22370
|
+
// Return early if already active — don't discard an in-progress connection
|
|
22371
|
+
const currentState = this.client?.getState();
|
|
22372
|
+
if (currentState === 'connected' || currentState === 'connecting' || currentState === 'reconnecting') {
|
|
20869
22373
|
return;
|
|
20870
22374
|
}
|
|
20871
22375
|
const sdkConfig = this.apiClient.getConfig();
|
|
@@ -20899,44 +22403,63 @@ class WalletEventsManager {
|
|
|
20899
22403
|
debug: this.config.debug,
|
|
20900
22404
|
tokenRefresher,
|
|
20901
22405
|
});
|
|
20902
|
-
|
|
20903
|
-
//
|
|
20904
|
-
|
|
20905
|
-
// Track previous state to detect reconnections and give-ups
|
|
20906
|
-
let previousState = 'connected';
|
|
22406
|
+
// ── Wire up all handlers BEFORE connecting ────────────────────────────
|
|
22407
|
+
// This ensures state changes and events are captured even if the initial
|
|
22408
|
+
// connect() times out and PersEventsClient retries in the background.
|
|
20907
22409
|
// Clean up previous state change listener (prevent duplicate listeners)
|
|
20908
22410
|
this.stateChangeUnsubscribe?.();
|
|
20909
|
-
//
|
|
22411
|
+
// Track whether a reconnect cycle is in progress.
|
|
22412
|
+
// Cannot use previousState === 'reconnecting' because PersEventsClient emits
|
|
22413
|
+
// 'connecting' between 'reconnecting' and 'connected', stomping previousState.
|
|
22414
|
+
let wasReconnecting = false;
|
|
20910
22415
|
this.stateChangeUnsubscribe = this.client.onStateChange((state) => {
|
|
20911
|
-
if (state === '
|
|
20912
|
-
|
|
22416
|
+
if (state === 'reconnecting') {
|
|
22417
|
+
wasReconnecting = true;
|
|
22418
|
+
}
|
|
22419
|
+
else if (state === 'disconnected' && wasReconnecting) {
|
|
22420
|
+
// Exhausted all reconnect attempts
|
|
20913
22421
|
this.reconnectGaveUp = true;
|
|
22422
|
+
wasReconnecting = false;
|
|
20914
22423
|
}
|
|
20915
22424
|
else if (state === 'connected') {
|
|
20916
22425
|
this.reconnectGaveUp = false;
|
|
20917
|
-
|
|
20918
|
-
|
|
22426
|
+
if (wasReconnecting) {
|
|
22427
|
+
wasReconnecting = false;
|
|
22428
|
+
// Emit reconnected event so apps can refresh state (catch missed events)
|
|
22429
|
+
// and re-subscribe wallets if the initial subscription never completed.
|
|
20919
22430
|
this.eventEmitter.emitSuccess({
|
|
20920
22431
|
type: 'wallet_reconnected',
|
|
20921
22432
|
domain: 'wallet',
|
|
20922
22433
|
userMessage: 'Wallet events reconnected',
|
|
20923
|
-
details: {
|
|
22434
|
+
details: {}
|
|
20924
22435
|
});
|
|
20925
22436
|
}
|
|
20926
22437
|
}
|
|
20927
|
-
previousState = state;
|
|
20928
22438
|
});
|
|
20929
22439
|
// Clear previous unsubscribes on new connection (prevent memory leak)
|
|
20930
22440
|
this.unsubscribes.forEach(unsub => unsub());
|
|
20931
22441
|
this.unsubscribes = [];
|
|
20932
22442
|
// Route all events through PersEventEmitter for unified stream
|
|
20933
22443
|
// Note: Cast needed because web3-types uses string for event.type while pers-shared uses strict union
|
|
20934
|
-
this.client.on('*', (event) => this.emitToPersEvents(event));
|
|
22444
|
+
this.unsubscribes.push(this.client.on('*', (event) => this.emitToPersEvents(event)));
|
|
20935
22445
|
// Re-attach any handlers registered before connect
|
|
20936
22446
|
for (const { type, handler } of this.pendingHandlers) {
|
|
20937
22447
|
this.unsubscribes.push(this.client.on(type, handler));
|
|
20938
22448
|
}
|
|
20939
22449
|
this.pendingHandlers = [];
|
|
22450
|
+
// Re-attach any notification handlers registered before connect
|
|
22451
|
+
for (const handler of this.pendingNotificationHandlers) {
|
|
22452
|
+
this.unsubscribes.push(this.client.onNotificationPending(handler));
|
|
22453
|
+
}
|
|
22454
|
+
this.pendingNotificationHandlers = [];
|
|
22455
|
+
// Rebind an explicitly-set notification channel across reconnects (new client instance each time)
|
|
22456
|
+
if (this.pendingNotificationChannelId) {
|
|
22457
|
+
this.client.bindNotificationChannel(this.pendingNotificationChannelId);
|
|
22458
|
+
}
|
|
22459
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
22460
|
+
await this.client.connect(token);
|
|
22461
|
+
// Reset gave-up flag on successful initial connection
|
|
22462
|
+
this.reconnectGaveUp = false;
|
|
20940
22463
|
}
|
|
20941
22464
|
/**
|
|
20942
22465
|
* Disconnect from real-time events
|
|
@@ -20961,6 +22484,8 @@ class WalletEventsManager {
|
|
|
20961
22484
|
this.authEventUnsubscribe = undefined;
|
|
20962
22485
|
this.reconnectGaveUp = false;
|
|
20963
22486
|
this.lastSubscriptions = { wallets: [], chains: [] };
|
|
22487
|
+
this.pendingNotificationHandlers = [];
|
|
22488
|
+
this.pendingNotificationChannelId = null;
|
|
20964
22489
|
}
|
|
20965
22490
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
20966
22491
|
// Subscription Methods (v1.2.0)
|
|
@@ -21046,6 +22571,47 @@ class WalletEventsManager {
|
|
|
21046
22571
|
return this.client?.getSubscriptionState() ?? { wallets: [], chains: [], activeChains: [] };
|
|
21047
22572
|
}
|
|
21048
22573
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
22574
|
+
// Notifications (Flow 1 — shares this same socket, see NotificationManager)
|
|
22575
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22576
|
+
/**
|
|
22577
|
+
* Bind this socket to a personal notification channelId (from `sdk.notifications.getChannelId()`).
|
|
22578
|
+
* Safe to call before or after connect() — queued and sent once the socket is open.
|
|
22579
|
+
*/
|
|
22580
|
+
bindNotificationChannel(channelId, topicIds) {
|
|
22581
|
+
this.pendingNotificationChannelId = channelId;
|
|
22582
|
+
this.client?.bindNotificationChannel(channelId, topicIds);
|
|
22583
|
+
}
|
|
22584
|
+
/**
|
|
22585
|
+
* Rebind the personal notification channelId on an already-connected socket.
|
|
22586
|
+
*/
|
|
22587
|
+
updateNotificationChannel(channelId) {
|
|
22588
|
+
this.pendingNotificationChannelId = channelId;
|
|
22589
|
+
this.client?.updateNotificationChannel(channelId);
|
|
22590
|
+
}
|
|
22591
|
+
/**
|
|
22592
|
+
* Get the notification routing context (channelId + topicIds) resolved for this socket.
|
|
22593
|
+
*/
|
|
22594
|
+
getNotificationContext() {
|
|
22595
|
+
return this.client?.getNotificationContext() ?? { notificationChannelId: null, topicIds: [] };
|
|
22596
|
+
}
|
|
22597
|
+
/**
|
|
22598
|
+
* Subscribe to real-time notification wake signals (`notification.pending`).
|
|
22599
|
+
* The signal carries no content — always follow up with `sdk.notifications.list()`.
|
|
22600
|
+
*/
|
|
22601
|
+
onNotificationPending(handler) {
|
|
22602
|
+
if (this.client) {
|
|
22603
|
+
const unsub = this.client.onNotificationPending(handler);
|
|
22604
|
+
this.unsubscribes.push(unsub);
|
|
22605
|
+
return unsub;
|
|
22606
|
+
}
|
|
22607
|
+
this.pendingNotificationHandlers.push(handler);
|
|
22608
|
+
return () => {
|
|
22609
|
+
const idx = this.pendingNotificationHandlers.indexOf(handler);
|
|
22610
|
+
if (idx !== -1)
|
|
22611
|
+
this.pendingNotificationHandlers.splice(idx, 1);
|
|
22612
|
+
};
|
|
22613
|
+
}
|
|
22614
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21049
22615
|
// Event Handlers
|
|
21050
22616
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
21051
22617
|
/**
|
|
@@ -21208,6 +22774,263 @@ class WalletEventsManager {
|
|
|
21208
22774
|
}
|
|
21209
22775
|
}
|
|
21210
22776
|
|
|
22777
|
+
/**
|
|
22778
|
+
* Notification API - Low-level API client for the notification inbox
|
|
22779
|
+
*
|
|
22780
|
+
* Backend routes (`src/notification/infrastructure/notifications.controller.ts`):
|
|
22781
|
+
* - `GET /notifications/channel-id` - Besu channelId for real-time WS binding
|
|
22782
|
+
* - `GET /notifications` - paginated inbox
|
|
22783
|
+
* - `GET /notifications/unread-count` - unread badge count
|
|
22784
|
+
* - `PATCH /notifications/:id/read` - mark as read
|
|
22785
|
+
* - `DELETE /notifications/:id` - delete
|
|
22786
|
+
* - `POST /notifications/send` - direct push (BUSINESS → USER, TENANT → USER|BUSINESS)
|
|
22787
|
+
*
|
|
22788
|
+
* @internal Use NotificationService or NotificationManager for higher-level operations
|
|
22789
|
+
*/
|
|
22790
|
+
class NotificationApi {
|
|
22791
|
+
constructor(apiClient) {
|
|
22792
|
+
this.apiClient = apiClient;
|
|
22793
|
+
this.basePath = '/notifications';
|
|
22794
|
+
}
|
|
22795
|
+
/**
|
|
22796
|
+
* Get the Besu channelId for the authenticated account (auto-provisioned on first call).
|
|
22797
|
+
* GET /notifications/channel-id
|
|
22798
|
+
*/
|
|
22799
|
+
async getChannelId() {
|
|
22800
|
+
const res = await this.apiClient.get(`${this.basePath}/channel-id`);
|
|
22801
|
+
return res.channelId;
|
|
22802
|
+
}
|
|
22803
|
+
/**
|
|
22804
|
+
* List the authenticated account's notification inbox (paginated).
|
|
22805
|
+
* GET /notifications
|
|
22806
|
+
*/
|
|
22807
|
+
async list(options) {
|
|
22808
|
+
const params = new URLSearchParams();
|
|
22809
|
+
if (options?.category)
|
|
22810
|
+
params.append('category', options.category);
|
|
22811
|
+
if (options?.unreadOnly !== undefined)
|
|
22812
|
+
params.append('unreadOnly', String(options.unreadOnly));
|
|
22813
|
+
if (options?.page)
|
|
22814
|
+
params.append('page', String(options.page));
|
|
22815
|
+
if (options?.limit)
|
|
22816
|
+
params.append('limit', String(options.limit));
|
|
22817
|
+
const queryString = params.toString();
|
|
22818
|
+
const url = queryString ? `${this.basePath}?${queryString}` : this.basePath;
|
|
22819
|
+
return this.apiClient.get(url);
|
|
22820
|
+
}
|
|
22821
|
+
/**
|
|
22822
|
+
* Get unread notification count.
|
|
22823
|
+
* GET /notifications/unread-count
|
|
22824
|
+
*/
|
|
22825
|
+
async getUnreadCount() {
|
|
22826
|
+
return this.apiClient.get(`${this.basePath}/unread-count`);
|
|
22827
|
+
}
|
|
22828
|
+
/**
|
|
22829
|
+
* Mark a notification as read.
|
|
22830
|
+
* PATCH /notifications/:id/read
|
|
22831
|
+
*/
|
|
22832
|
+
async markAsRead(id) {
|
|
22833
|
+
return this.apiClient.patch(`${this.basePath}/${id}/read`);
|
|
22834
|
+
}
|
|
22835
|
+
/**
|
|
22836
|
+
* Delete a notification.
|
|
22837
|
+
* DELETE /notifications/:id
|
|
22838
|
+
*/
|
|
22839
|
+
async delete(id) {
|
|
22840
|
+
await this.apiClient.delete(`${this.basePath}/${id}`);
|
|
22841
|
+
}
|
|
22842
|
+
/**
|
|
22843
|
+
* Send a direct notification (BUSINESS → USER, or TENANT → USER|BUSINESS).
|
|
22844
|
+
* POST /notifications/send
|
|
22845
|
+
*/
|
|
22846
|
+
async send(dto) {
|
|
22847
|
+
return this.apiClient.post(`${this.basePath}/send`, dto);
|
|
22848
|
+
}
|
|
22849
|
+
}
|
|
22850
|
+
|
|
22851
|
+
/**
|
|
22852
|
+
* Notification Service - Business logic layer for the notification inbox
|
|
22853
|
+
*
|
|
22854
|
+
* Caches the resolved `channelId` per session (stable per-recipient per-tenant,
|
|
22855
|
+
* per the SDK integration guide) to avoid refetching it on every WS (re)connect.
|
|
22856
|
+
*
|
|
22857
|
+
* @group Services
|
|
22858
|
+
* @category Notification
|
|
22859
|
+
*/
|
|
22860
|
+
class NotificationService {
|
|
22861
|
+
constructor(notificationApi) {
|
|
22862
|
+
this.notificationApi = notificationApi;
|
|
22863
|
+
}
|
|
22864
|
+
/**
|
|
22865
|
+
* Get the Besu channelId for the authenticated account.
|
|
22866
|
+
* Cached in-memory for the lifetime of this service instance.
|
|
22867
|
+
*/
|
|
22868
|
+
async getChannelId(forceRefresh = false) {
|
|
22869
|
+
if (!forceRefresh && this.cachedChannelId !== undefined) {
|
|
22870
|
+
return this.cachedChannelId;
|
|
22871
|
+
}
|
|
22872
|
+
this.cachedChannelId = await this.notificationApi.getChannelId();
|
|
22873
|
+
return this.cachedChannelId;
|
|
22874
|
+
}
|
|
22875
|
+
async getNotifications(options) {
|
|
22876
|
+
return this.notificationApi.list(options);
|
|
22877
|
+
}
|
|
22878
|
+
async getUnreadCount() {
|
|
22879
|
+
return this.notificationApi.getUnreadCount();
|
|
22880
|
+
}
|
|
22881
|
+
async markAsRead(id) {
|
|
22882
|
+
if (!id?.trim()) {
|
|
22883
|
+
throw new Error('Notification id is required');
|
|
22884
|
+
}
|
|
22885
|
+
return this.notificationApi.markAsRead(id);
|
|
22886
|
+
}
|
|
22887
|
+
async deleteNotification(id) {
|
|
22888
|
+
if (!id?.trim()) {
|
|
22889
|
+
throw new Error('Notification id is required');
|
|
22890
|
+
}
|
|
22891
|
+
return this.notificationApi.delete(id);
|
|
22892
|
+
}
|
|
22893
|
+
async sendNotification(dto) {
|
|
22894
|
+
if (!dto.recipientId?.trim()) {
|
|
22895
|
+
throw new Error('recipientId is required');
|
|
22896
|
+
}
|
|
22897
|
+
if (!dto.title?.trim()) {
|
|
22898
|
+
throw new Error('title is required');
|
|
22899
|
+
}
|
|
22900
|
+
return this.notificationApi.send(dto);
|
|
22901
|
+
}
|
|
22902
|
+
}
|
|
22903
|
+
|
|
22904
|
+
/**
|
|
22905
|
+
* Notification Manager - Clean, high-level interface for the notification inbox (Flow 1)
|
|
22906
|
+
*
|
|
22907
|
+
* Notifications are delivered through two independent channels — use both together:
|
|
22908
|
+
* - **REST inbox** (this manager): source of truth, full content, pagination, read/unread state
|
|
22909
|
+
* - **WS Relay `notification.pending`** (`onPending()`): real-time wake signal, no content —
|
|
22910
|
+
* always follow up with a REST fetch
|
|
22911
|
+
*
|
|
22912
|
+
* Real-time delivery **shares the same WebSocket connection** as `sdk.walletEvents` — no
|
|
22913
|
+
* second socket is opened. Call `connectRealtime()` once (after auth) to bind the channel.
|
|
22914
|
+
*
|
|
22915
|
+
* @group Managers
|
|
22916
|
+
* @category Notification
|
|
22917
|
+
*
|
|
22918
|
+
* @example Recommended app flow
|
|
22919
|
+
* ```typescript
|
|
22920
|
+
* await sdk.notifications.connectRealtime();
|
|
22921
|
+
* const inbox = await sdk.notifications.list({ unreadOnly: true });
|
|
22922
|
+
* const { count } = await sdk.notifications.getUnreadCount();
|
|
22923
|
+
*
|
|
22924
|
+
* sdk.notifications.onPending(async () => {
|
|
22925
|
+
* // Signal is a hint — always re-fetch for actual content
|
|
22926
|
+
* await sdk.notifications.list();
|
|
22927
|
+
* await sdk.notifications.getUnreadCount();
|
|
22928
|
+
* });
|
|
22929
|
+
* ```
|
|
22930
|
+
*
|
|
22931
|
+
* @example Business → user direct push
|
|
22932
|
+
* ```typescript
|
|
22933
|
+
* await sdk.notifications.send({
|
|
22934
|
+
* recipientType: 'user',
|
|
22935
|
+
* recipientId: userId,
|
|
22936
|
+
* title: 'New reward available!',
|
|
22937
|
+
* body: "You've earned 100 points.",
|
|
22938
|
+
* category: 'campaign',
|
|
22939
|
+
* entityId: campaignId,
|
|
22940
|
+
* });
|
|
22941
|
+
* ```
|
|
22942
|
+
*/
|
|
22943
|
+
class NotificationManager {
|
|
22944
|
+
constructor(apiClient, walletEvents, events) {
|
|
22945
|
+
this.walletEvents = walletEvents;
|
|
22946
|
+
this.events = events;
|
|
22947
|
+
this.notificationService = new NotificationService(new NotificationApi(apiClient));
|
|
22948
|
+
}
|
|
22949
|
+
// ================================
|
|
22950
|
+
// REST Inbox
|
|
22951
|
+
// ================================
|
|
22952
|
+
/**
|
|
22953
|
+
* Get the Besu channelId for the authenticated account (auto-provisioned on first call).
|
|
22954
|
+
* Cached in-memory; pass `forceRefresh` to bypass the cache (e.g. after re-auth).
|
|
22955
|
+
*/
|
|
22956
|
+
async getChannelId(forceRefresh = false) {
|
|
22957
|
+
return this.notificationService.getChannelId(forceRefresh);
|
|
22958
|
+
}
|
|
22959
|
+
/**
|
|
22960
|
+
* List the authenticated account's notification inbox (paginated).
|
|
22961
|
+
*/
|
|
22962
|
+
async list(options) {
|
|
22963
|
+
return this.notificationService.getNotifications(options);
|
|
22964
|
+
}
|
|
22965
|
+
/**
|
|
22966
|
+
* Get unread notification count (for a badge).
|
|
22967
|
+
*/
|
|
22968
|
+
async getUnreadCount() {
|
|
22969
|
+
return this.notificationService.getUnreadCount();
|
|
22970
|
+
}
|
|
22971
|
+
/**
|
|
22972
|
+
* Mark a notification as read.
|
|
22973
|
+
*/
|
|
22974
|
+
async markAsRead(id) {
|
|
22975
|
+
return this.notificationService.markAsRead(id);
|
|
22976
|
+
}
|
|
22977
|
+
/**
|
|
22978
|
+
* Delete a notification.
|
|
22979
|
+
*/
|
|
22980
|
+
async delete(id) {
|
|
22981
|
+
return this.notificationService.deleteNotification(id);
|
|
22982
|
+
}
|
|
22983
|
+
/**
|
|
22984
|
+
* Send a direct notification. BUSINESS accounts may only target `recipientType: 'user'`;
|
|
22985
|
+
* TENANT (admin) accounts may target `'user'` or `'business'`.
|
|
22986
|
+
*/
|
|
22987
|
+
async send(dto) {
|
|
22988
|
+
const result = await this.notificationService.sendNotification(dto);
|
|
22989
|
+
this.events?.emitSuccess({
|
|
22990
|
+
domain: 'notification',
|
|
22991
|
+
type: 'notification_sent',
|
|
22992
|
+
userMessage: 'Notification sent successfully',
|
|
22993
|
+
details: { notificationId: result.id, recipientType: result.recipientType }
|
|
22994
|
+
});
|
|
22995
|
+
return result;
|
|
22996
|
+
}
|
|
22997
|
+
// ================================
|
|
22998
|
+
// Real-Time (shares sdk.walletEvents' socket)
|
|
22999
|
+
// ================================
|
|
23000
|
+
/**
|
|
23001
|
+
* Resolve this account's channelId and bind it on the shared wallet-events socket,
|
|
23002
|
+
* connecting first if needed. Call once after authentication.
|
|
23003
|
+
*/
|
|
23004
|
+
async connectRealtime() {
|
|
23005
|
+
await this.walletEvents.connect();
|
|
23006
|
+
const channelId = await this.getChannelId();
|
|
23007
|
+
if (channelId) {
|
|
23008
|
+
this.walletEvents.bindNotificationChannel(channelId);
|
|
23009
|
+
}
|
|
23010
|
+
}
|
|
23011
|
+
/**
|
|
23012
|
+
* Get the notification routing context (channelId + topicIds) resolved for the socket.
|
|
23013
|
+
*/
|
|
23014
|
+
getNotificationContext() {
|
|
23015
|
+
return this.walletEvents.getNotificationContext();
|
|
23016
|
+
}
|
|
23017
|
+
/**
|
|
23018
|
+
* Subscribe to real-time notification wake signals (`notification.pending`).
|
|
23019
|
+
* The signal carries no content — always follow up with `list()` / `getUnreadCount()`.
|
|
23020
|
+
*/
|
|
23021
|
+
onPending(handler) {
|
|
23022
|
+
return this.walletEvents.onNotificationPending((info) => {
|
|
23023
|
+
this.events?.emitSuccess({
|
|
23024
|
+
domain: 'notification',
|
|
23025
|
+
type: 'notification_pending',
|
|
23026
|
+
userMessage: 'A new notification arrived',
|
|
23027
|
+
details: { messageId: info.messageId, category: info.notificationCategory }
|
|
23028
|
+
});
|
|
23029
|
+
handler(info);
|
|
23030
|
+
});
|
|
23031
|
+
}
|
|
23032
|
+
}
|
|
23033
|
+
|
|
21211
23034
|
/**
|
|
21212
23035
|
* Custom Field Definitions API
|
|
21213
23036
|
*
|
|
@@ -21225,11 +23048,12 @@ class CustomFieldApi {
|
|
|
21225
23048
|
* List all custom field definitions
|
|
21226
23049
|
* Backend returns non-paginated array sorted by sortOrder
|
|
21227
23050
|
*/
|
|
21228
|
-
async getDefinitions(entityType) {
|
|
23051
|
+
async getDefinitions(entityType, include) {
|
|
21229
23052
|
const params = new URLSearchParams();
|
|
21230
23053
|
if (entityType) {
|
|
21231
23054
|
params.append('entityType', entityType);
|
|
21232
23055
|
}
|
|
23056
|
+
setIncludeParam(params, include);
|
|
21233
23057
|
const query = params.toString();
|
|
21234
23058
|
const url = query ? `${this.basePath}?${query}` : this.basePath;
|
|
21235
23059
|
return this.apiClient.get(url);
|
|
@@ -21237,8 +23061,11 @@ class CustomFieldApi {
|
|
|
21237
23061
|
/**
|
|
21238
23062
|
* Get a single custom field definition by ID
|
|
21239
23063
|
*/
|
|
21240
|
-
async getDefinition(id) {
|
|
21241
|
-
|
|
23064
|
+
async getDefinition(id, include) {
|
|
23065
|
+
const params = new URLSearchParams();
|
|
23066
|
+
setIncludeParam(params, include);
|
|
23067
|
+
const query = params.toString();
|
|
23068
|
+
return this.apiClient.get(`${this.basePath}/${id}${query ? `?${query}` : ''}`);
|
|
21242
23069
|
}
|
|
21243
23070
|
/**
|
|
21244
23071
|
* Create a new custom field definition
|
|
@@ -21350,7 +23177,7 @@ class CustomFieldDefinitionManager {
|
|
|
21350
23177
|
* ```
|
|
21351
23178
|
*/
|
|
21352
23179
|
async getDefinitions(options) {
|
|
21353
|
-
return this.api.getDefinitions(options?.entityType);
|
|
23180
|
+
return this.api.getDefinitions(options?.entityType, options?.include);
|
|
21354
23181
|
}
|
|
21355
23182
|
/**
|
|
21356
23183
|
* Get a single custom field definition by ID
|
|
@@ -21371,8 +23198,8 @@ class CustomFieldDefinitionManager {
|
|
|
21371
23198
|
* }
|
|
21372
23199
|
* ```
|
|
21373
23200
|
*/
|
|
21374
|
-
async getDefinition(id) {
|
|
21375
|
-
return this.api.getDefinition(id);
|
|
23201
|
+
async getDefinition(id, include) {
|
|
23202
|
+
return this.api.getDefinition(id, include);
|
|
21376
23203
|
}
|
|
21377
23204
|
/**
|
|
21378
23205
|
* Create a new custom field definition
|
|
@@ -22233,6 +24060,16 @@ class PersSDK {
|
|
|
22233
24060
|
console.warn('[PersSDK] Failed to auto-connect wallet events:', err.message);
|
|
22234
24061
|
});
|
|
22235
24062
|
}
|
|
24063
|
+
// Re-subscribe after reconnect. This covers two cases:
|
|
24064
|
+
// 1. Normal reconnect after a drop (restoreSubscriptions handles it, but this
|
|
24065
|
+
// also refreshes the subscription from the latest user state).
|
|
24066
|
+
// 2. Initial connection timed out → client auto-reconnected → subscription
|
|
24067
|
+
// was never sent because connectWalletEvents() failed before subscribeWallets().
|
|
24068
|
+
if (event.level === 'success' && event.type === 'wallet_reconnected') {
|
|
24069
|
+
this.connectWalletEvents().catch((err) => {
|
|
24070
|
+
console.warn('[PersSDK] Failed to re-subscribe wallet events after reconnect:', err.message);
|
|
24071
|
+
});
|
|
24072
|
+
}
|
|
22236
24073
|
// Disconnect on logout
|
|
22237
24074
|
if (event.level === 'success' && event.type === 'logout_success') {
|
|
22238
24075
|
this.walletEvents.disconnect();
|
|
@@ -22640,6 +24477,18 @@ class PersSDK {
|
|
|
22640
24477
|
}
|
|
22641
24478
|
return this._tenants;
|
|
22642
24479
|
}
|
|
24480
|
+
/**
|
|
24481
|
+
* IPFS manager - Gateway, URL and token metadata resolution (chain-agnostic, no web3 dependencies)
|
|
24482
|
+
*
|
|
24483
|
+
* @example
|
|
24484
|
+
* ```typescript
|
|
24485
|
+
* const url = await sdk.ipfs.resolveUrl('ipfs://QmXxx...');
|
|
24486
|
+
* const metadata = await sdk.ipfs.fetchMetadata('ipfs://QmXxx.../metadata.json');
|
|
24487
|
+
* ```
|
|
24488
|
+
*/
|
|
24489
|
+
get ipfs() {
|
|
24490
|
+
return this.tenants.ipfs;
|
|
24491
|
+
}
|
|
22643
24492
|
/**
|
|
22644
24493
|
* API Key manager - High-level API key management operations (Admin Only)
|
|
22645
24494
|
*
|
|
@@ -22729,6 +24578,35 @@ class PersSDK {
|
|
|
22729
24578
|
}
|
|
22730
24579
|
return this._triggerSources;
|
|
22731
24580
|
}
|
|
24581
|
+
/**
|
|
24582
|
+
* Redirect manager - High-level redirect (QR code / short-link) operations
|
|
24583
|
+
*
|
|
24584
|
+
* Provides methods for creating short-code redirects, updating their target
|
|
24585
|
+
* URL/label, and retrieving per-scan analytics (device, day, channel breakdowns).
|
|
24586
|
+
*
|
|
24587
|
+
* @returns RedirectManager instance
|
|
24588
|
+
*
|
|
24589
|
+
* @example Redirect Operations
|
|
24590
|
+
* ```typescript
|
|
24591
|
+
* // Create a redirect
|
|
24592
|
+
* const redirect = await sdk.redirects.create({
|
|
24593
|
+
* targetUrl: 'https://app.pers.ninja/campaigns/abc',
|
|
24594
|
+
* label: 'Summer Campaign QR'
|
|
24595
|
+
* });
|
|
24596
|
+
*
|
|
24597
|
+
* // Build the QR target URL
|
|
24598
|
+
* const qrUrl = sdk.redirects.getQrUrl(redirect.code);
|
|
24599
|
+
*
|
|
24600
|
+
* // Get scan analytics
|
|
24601
|
+
* const stats = await sdk.redirects.getAnalytics(redirect.id, { from: '2025-01-01' });
|
|
24602
|
+
* ```
|
|
24603
|
+
*/
|
|
24604
|
+
get redirects() {
|
|
24605
|
+
if (!this._redirects) {
|
|
24606
|
+
this._redirects = new RedirectManager(this.apiClient, this._events);
|
|
24607
|
+
}
|
|
24608
|
+
return this._redirects;
|
|
24609
|
+
}
|
|
22732
24610
|
/**
|
|
22733
24611
|
* Webhook manager - High-level webhook operations
|
|
22734
24612
|
*
|
|
@@ -22898,6 +24776,39 @@ class PersSDK {
|
|
|
22898
24776
|
// Reset manager so it picks up new config
|
|
22899
24777
|
this._walletEvents = undefined;
|
|
22900
24778
|
}
|
|
24779
|
+
/**
|
|
24780
|
+
* Notification Manager - Per-recipient notification inbox (Flow 1 — transactional/private)
|
|
24781
|
+
*
|
|
24782
|
+
* Notifications are delivered through two independent channels — use both together:
|
|
24783
|
+
* - **REST inbox** (`list`, `getUnreadCount`, `markAsRead`, `delete`): source of truth
|
|
24784
|
+
* - **WS Relay `notification.pending`** (`onPending`): real-time wake signal only
|
|
24785
|
+
*
|
|
24786
|
+
* Real-time delivery shares the same WebSocket connection as `sdk.walletEvents` —
|
|
24787
|
+
* calling `connectRealtime()` connects it (if needed) and binds the channelId.
|
|
24788
|
+
*
|
|
24789
|
+
* @returns NotificationManager instance
|
|
24790
|
+
*
|
|
24791
|
+
* @example Basic Usage
|
|
24792
|
+
* ```typescript
|
|
24793
|
+
* await sdk.notifications.connectRealtime();
|
|
24794
|
+
*
|
|
24795
|
+
* const inbox = await sdk.notifications.list({ unreadOnly: true });
|
|
24796
|
+
* const { count } = await sdk.notifications.getUnreadCount();
|
|
24797
|
+
*
|
|
24798
|
+
* sdk.notifications.onPending(() => {
|
|
24799
|
+
* // Signal carries no content — re-fetch the inbox
|
|
24800
|
+
* sdk.notifications.list();
|
|
24801
|
+
* });
|
|
24802
|
+
* ```
|
|
24803
|
+
*
|
|
24804
|
+
* @see {@link NotificationManager} for detailed documentation
|
|
24805
|
+
*/
|
|
24806
|
+
get notifications() {
|
|
24807
|
+
if (!this._notifications) {
|
|
24808
|
+
this._notifications = new NotificationManager(this.apiClient, this.walletEvents, this._events);
|
|
24809
|
+
}
|
|
24810
|
+
return this._notifications;
|
|
24811
|
+
}
|
|
22901
24812
|
// ==========================================
|
|
22902
24813
|
// DATA SOURCE TRACKING
|
|
22903
24814
|
// ==========================================
|
|
@@ -22966,6 +24877,30 @@ class PersSDK {
|
|
|
22966
24877
|
setPlatform(platform) {
|
|
22967
24878
|
this.apiClient.setPlatform(platform);
|
|
22968
24879
|
}
|
|
24880
|
+
// ==========================================
|
|
24881
|
+
// CONTENT LOCALE
|
|
24882
|
+
// ==========================================
|
|
24883
|
+
/**
|
|
24884
|
+
* Set the content locale for localized API responses and IPFS token metadata
|
|
24885
|
+
*
|
|
24886
|
+
* Opt-in: until set (or configured via `PersConfig.locale`), base-language strings are returned.
|
|
24887
|
+
* Already-loaded content is not re-translated: refetch (or invalidate app caches) after changing it.
|
|
24888
|
+
*
|
|
24889
|
+
* @param locale - A BCP 47 tag (e.g. 'de-DE'), 'auto' for the device locale, or null to disable
|
|
24890
|
+
*
|
|
24891
|
+
* @example
|
|
24892
|
+
* ```typescript
|
|
24893
|
+
* sdk.setLocale('fr-FR');
|
|
24894
|
+
* const campaigns = await sdk.campaigns.getActiveCampaigns();
|
|
24895
|
+
* ```
|
|
24896
|
+
*/
|
|
24897
|
+
setLocale(locale) {
|
|
24898
|
+
this.apiClient.setLocale(locale);
|
|
24899
|
+
}
|
|
24900
|
+
/** Normalized content locale in effect, or null when disabled */
|
|
24901
|
+
getLocale() {
|
|
24902
|
+
return this.apiClient.getLocale();
|
|
24903
|
+
}
|
|
22969
24904
|
/**
|
|
22970
24905
|
* Gets the API client for direct PERS API requests
|
|
22971
24906
|
*
|
|
@@ -23373,17 +25308,30 @@ class ReactNativeSecureStorage {
|
|
|
23373
25308
|
async get(key) {
|
|
23374
25309
|
const prefixedKey = this.getKeyName(key);
|
|
23375
25310
|
if (this.SECURE_KEYS.has(key)) {
|
|
23376
|
-
|
|
23377
|
-
|
|
23378
|
-
|
|
23379
|
-
|
|
23380
|
-
|
|
25311
|
+
// The iOS Keychain can be briefly unreadable (device locked, background launch), so retry before giving up
|
|
25312
|
+
let keychainError;
|
|
25313
|
+
if (Keychain) {
|
|
25314
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
25315
|
+
try {
|
|
25316
|
+
const credentials = await Keychain.getGenericPassword({ service: prefixedKey });
|
|
25317
|
+
if (credentials) {
|
|
25318
|
+
return this.tryParse(credentials.password);
|
|
25319
|
+
}
|
|
25320
|
+
keychainError = undefined;
|
|
25321
|
+
break;
|
|
25322
|
+
}
|
|
25323
|
+
catch (e) {
|
|
25324
|
+
keychainError = e;
|
|
25325
|
+
if (attempt < 2)
|
|
25326
|
+
await new Promise(resolve => setTimeout(resolve, 150 * (attempt + 1)));
|
|
23381
25327
|
}
|
|
23382
25328
|
}
|
|
23383
25329
|
}
|
|
23384
|
-
|
|
23385
|
-
console.warn(`[ReactNativeSecureStorage] Failed to access keychain for ${key}`,
|
|
23386
|
-
//
|
|
25330
|
+
if (keychainError) {
|
|
25331
|
+
console.warn(`[ReactNativeSecureStorage] Failed to access keychain for ${key}`, keychainError);
|
|
25332
|
+
// Returning "missing" here would make the DPoP manager regenerate the key pair and log the user out
|
|
25333
|
+
if (key === DPOP_STORAGE_KEYS.PRIVATE)
|
|
25334
|
+
throw keychainError;
|
|
23387
25335
|
}
|
|
23388
25336
|
// Fallback: Check AsyncStorage if not found in Keychain or Keychain failed
|
|
23389
25337
|
try {
|
|
@@ -23718,6 +25666,9 @@ class ReactNativeHttpClient {
|
|
|
23718
25666
|
async put(url, body, options) {
|
|
23719
25667
|
return this.request('PUT', url, body, options);
|
|
23720
25668
|
}
|
|
25669
|
+
async patch(url, body, options) {
|
|
25670
|
+
return this.request('PATCH', url, body, options);
|
|
25671
|
+
}
|
|
23721
25672
|
async delete(url, options) {
|
|
23722
25673
|
return this.request('DELETE', url, undefined, options);
|
|
23723
25674
|
}
|
|
@@ -23736,8 +25687,8 @@ class ReactNativeHttpClient {
|
|
|
23736
25687
|
config.signal = controller.signal;
|
|
23737
25688
|
setTimeout(() => controller.abort(), options.timeout);
|
|
23738
25689
|
}
|
|
23739
|
-
// Add body for POST/PUT requests
|
|
23740
|
-
if (body && (method === 'POST' || method === 'PUT')) {
|
|
25690
|
+
// Add body for POST/PUT/PATCH requests
|
|
25691
|
+
if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
|
|
23741
25692
|
config.body = JSON.stringify(body);
|
|
23742
25693
|
}
|
|
23743
25694
|
try {
|
|
@@ -23893,6 +25844,7 @@ const PersSDKProvider = ({ children, config }) => {
|
|
|
23893
25844
|
const [isInitialized, setIsInitialized] = react.useState(false);
|
|
23894
25845
|
const [isAuthenticated, setIsAuthenticated] = react.useState(false);
|
|
23895
25846
|
const [user, setUser] = react.useState(null);
|
|
25847
|
+
const [locale, setLocaleState] = react.useState(null);
|
|
23896
25848
|
// Keep state refs in sync immediately (not in useEffect to avoid race conditions)
|
|
23897
25849
|
sdkRef.current = sdk;
|
|
23898
25850
|
isInitializedRef.current = isInitialized;
|
|
@@ -23970,6 +25922,7 @@ const PersSDKProvider = ({ children, config }) => {
|
|
|
23970
25922
|
}, { domains: ['authentication'], replay: true });
|
|
23971
25923
|
setAuthProvider(authProvider);
|
|
23972
25924
|
setSdk(sdkInstance);
|
|
25925
|
+
setLocaleState(sdkInstance.getLocale());
|
|
23973
25926
|
setIsInitialized(true);
|
|
23974
25927
|
}
|
|
23975
25928
|
catch (error) {
|
|
@@ -24002,6 +25955,14 @@ const PersSDKProvider = ({ children, config }) => {
|
|
|
24002
25955
|
});
|
|
24003
25956
|
}
|
|
24004
25957
|
}, [config, isInitialized, initialize]);
|
|
25958
|
+
const setLocale = react.useCallback((next) => {
|
|
25959
|
+
const currentSdk = sdkRef.current;
|
|
25960
|
+
if (!currentSdk) {
|
|
25961
|
+
throw new Error('SDK not initialized. Call initialize() first.');
|
|
25962
|
+
}
|
|
25963
|
+
currentSdk.setLocale(next);
|
|
25964
|
+
setLocaleState(currentSdk.getLocale());
|
|
25965
|
+
}, []);
|
|
24005
25966
|
const refreshUserData = react.useCallback(async () => {
|
|
24006
25967
|
// Read from refs to get current values
|
|
24007
25968
|
const currentSdk = sdkRef.current;
|
|
@@ -24068,8 +26029,10 @@ const PersSDKProvider = ({ children, config }) => {
|
|
|
24068
26029
|
isInitialized,
|
|
24069
26030
|
isAuthenticated,
|
|
24070
26031
|
user,
|
|
26032
|
+
locale,
|
|
24071
26033
|
// Methods - expose functions directly, not through refs
|
|
24072
26034
|
initialize,
|
|
26035
|
+
setLocale,
|
|
24073
26036
|
setAuthenticationState,
|
|
24074
26037
|
refreshUserData,
|
|
24075
26038
|
restoreSession,
|
|
@@ -24079,7 +26042,9 @@ const PersSDKProvider = ({ children, config }) => {
|
|
|
24079
26042
|
isInitialized,
|
|
24080
26043
|
isAuthenticated,
|
|
24081
26044
|
user,
|
|
26045
|
+
locale,
|
|
24082
26046
|
initialize,
|
|
26047
|
+
setLocale,
|
|
24083
26048
|
setAuthenticationState,
|
|
24084
26049
|
refreshUserData,
|
|
24085
26050
|
restoreSession
|
|
@@ -44438,9 +46403,9 @@ class Web3ChainService {
|
|
|
44438
46403
|
* - Metadata: CACHED 24h - IPFS content is immutable
|
|
44439
46404
|
*/
|
|
44440
46405
|
class TokenDomainService {
|
|
44441
|
-
constructor(web3Api,
|
|
46406
|
+
constructor(web3Api, metadataSource, contractService) {
|
|
44442
46407
|
this.web3Api = web3Api;
|
|
44443
|
-
this.
|
|
46408
|
+
this.metadataSource = metadataSource;
|
|
44444
46409
|
this.contractService = contractService;
|
|
44445
46410
|
this.cache = globalCacheService.createNamespace('token-domain');
|
|
44446
46411
|
}
|
|
@@ -44461,7 +46426,7 @@ class TokenDomainService {
|
|
|
44461
46426
|
const abi = convertAbiToInterface(params.abi);
|
|
44462
46427
|
const tokenUri = await this.web3Api.getTokenUri({ ...params, abi });
|
|
44463
46428
|
const metadata = tokenUri
|
|
44464
|
-
? await this.
|
|
46429
|
+
? await this.metadataSource.fetchAndProcessMetadata(tokenUri)
|
|
44465
46430
|
: null;
|
|
44466
46431
|
return { tokenId: params.tokenId, tokenUri, metadata };
|
|
44467
46432
|
}
|
|
@@ -44643,47 +46608,6 @@ class TokenDomainService {
|
|
|
44643
46608
|
}
|
|
44644
46609
|
}
|
|
44645
46610
|
|
|
44646
|
-
/**
|
|
44647
|
-
* MetadataDomainService - Clean IPFS metadata resolution with centralized caching
|
|
44648
|
-
*
|
|
44649
|
-
* IPFS gateway is resolved from tenant configuration (chain-agnostic).
|
|
44650
|
-
*/
|
|
44651
|
-
class MetadataDomainService {
|
|
44652
|
-
constructor(ipfsApi) {
|
|
44653
|
-
this.ipfsApi = ipfsApi;
|
|
44654
|
-
this.cache = globalCacheService.createNamespace('metadata');
|
|
44655
|
-
}
|
|
44656
|
-
async fetchAndProcessMetadata(tokenUri) {
|
|
44657
|
-
const cacheKey = `fetch:${tokenUri}`;
|
|
44658
|
-
return this.cache.getOrSet(cacheKey, async () => {
|
|
44659
|
-
try {
|
|
44660
|
-
return await this.ipfsApi.fetchAndProcessMetadata(tokenUri);
|
|
44661
|
-
}
|
|
44662
|
-
catch (error) {
|
|
44663
|
-
console.error(`Error fetching metadata for ${tokenUri}:`, error);
|
|
44664
|
-
return null;
|
|
44665
|
-
}
|
|
44666
|
-
}, CacheTTL.METADATA);
|
|
44667
|
-
}
|
|
44668
|
-
async resolveIPFSUrl(url) {
|
|
44669
|
-
const cacheKey = `resolve:${url}`;
|
|
44670
|
-
return this.cache.getOrSet(cacheKey, () => this.ipfsApi.resolveIPFSUrl(url), CacheTTL.GATEWAY);
|
|
44671
|
-
}
|
|
44672
|
-
/**
|
|
44673
|
-
* Get IPFS gateway domain from tenant configuration.
|
|
44674
|
-
* Useful for pipes that need to resolve URLs synchronously after initial fetch.
|
|
44675
|
-
*/
|
|
44676
|
-
async getIpfsGatewayDomain() {
|
|
44677
|
-
return this.ipfsApi.getIpfsGatewayDomain();
|
|
44678
|
-
}
|
|
44679
|
-
/**
|
|
44680
|
-
* Clear all cached metadata and URLs for this service
|
|
44681
|
-
*/
|
|
44682
|
-
clearCache() {
|
|
44683
|
-
this.cache.clear();
|
|
44684
|
-
}
|
|
44685
|
-
}
|
|
44686
|
-
|
|
44687
46611
|
/**
|
|
44688
46612
|
* ContractDomainService - Clean contract analysis without external dependencies
|
|
44689
46613
|
*/
|
|
@@ -44761,7 +46685,8 @@ class TokenCollectionManager {
|
|
|
44761
46685
|
this.config = {
|
|
44762
46686
|
autoRefreshOnWalletEvents: config?.autoRefreshOnWalletEvents ?? true,
|
|
44763
46687
|
eventDebounceMs: config?.eventDebounceMs ?? 500,
|
|
44764
|
-
debug: config?.debug ?? false
|
|
46688
|
+
debug: config?.debug ?? false,
|
|
46689
|
+
getLocale: config?.getLocale ?? (() => null)
|
|
44765
46690
|
};
|
|
44766
46691
|
if (this.config.autoRefreshOnWalletEvents && eventEmitter) {
|
|
44767
46692
|
this.setupEventSubscription();
|
|
@@ -44791,10 +46716,10 @@ class TokenCollectionManager {
|
|
|
44791
46716
|
// Return cached if available and not stale
|
|
44792
46717
|
if (cached?.lastCollection) {
|
|
44793
46718
|
this.log(`Cache HIT for ${key}`);
|
|
44794
|
-
return cached.lastCollection;
|
|
46719
|
+
return localizeTokenCollection(cached.lastCollection, this.config.getLocale());
|
|
44795
46720
|
}
|
|
44796
46721
|
this.log(`Cache MISS for ${key}, fetching...`);
|
|
44797
|
-
return this.fetchAndCache(params);
|
|
46722
|
+
return localizeTokenCollection(await this.fetchAndCache(params), this.config.getLocale());
|
|
44798
46723
|
}
|
|
44799
46724
|
/**
|
|
44800
46725
|
* Force refresh a specific collection
|
|
@@ -44836,7 +46761,7 @@ class TokenCollectionManager {
|
|
|
44836
46761
|
return Array.from(this.collections.entries()).map(([_, entry]) => ({
|
|
44837
46762
|
contractAddress: entry.params.contractAddress,
|
|
44838
46763
|
accountAddress: entry.params.accountAddress,
|
|
44839
|
-
collection: entry.lastCollection
|
|
46764
|
+
collection: entry.lastCollection ? localizeTokenCollection(entry.lastCollection, this.config.getLocale()) : null
|
|
44840
46765
|
}));
|
|
44841
46766
|
}
|
|
44842
46767
|
/**
|
|
@@ -44957,7 +46882,7 @@ class TokenCollectionManager {
|
|
|
44957
46882
|
// Notify subscribers
|
|
44958
46883
|
for (const handler of this.handlers) {
|
|
44959
46884
|
try {
|
|
44960
|
-
handler(newCollection, entry.params.contractAddress, {
|
|
46885
|
+
handler(localizeTokenCollection(newCollection, this.config.getLocale()), entry.params.contractAddress, {
|
|
44961
46886
|
type: source === 'refresh' ? 'refresh' : 'wallet_event',
|
|
44962
46887
|
source
|
|
44963
46888
|
});
|
|
@@ -45342,23 +47267,25 @@ class BalanceManager {
|
|
|
45342
47267
|
* Simplified architecture with concrete classes
|
|
45343
47268
|
*/
|
|
45344
47269
|
class Web3ApplicationService {
|
|
45345
|
-
constructor(web3Api, ipfsApi) {
|
|
47270
|
+
constructor(web3Api, ipfsApi, getLocale = () => null) {
|
|
47271
|
+
this.ipfsApi = ipfsApi;
|
|
47272
|
+
this.getLocale = getLocale;
|
|
45346
47273
|
// Create domain services with injected infrastructure dependencies
|
|
45347
47274
|
this.contractDomainService = new ContractDomainService();
|
|
45348
|
-
this.
|
|
45349
|
-
this.tokenDomainService = new TokenDomainService(web3Api, this.metadataDomainService, this.contractDomainService);
|
|
47275
|
+
this.tokenDomainService = new TokenDomainService(web3Api, this.ipfsApi, this.contractDomainService);
|
|
45350
47276
|
}
|
|
45351
47277
|
/**
|
|
45352
47278
|
* Get balance and metadata for a specific token
|
|
45353
47279
|
*/
|
|
45354
47280
|
async getSpecificTokenBalance(request) {
|
|
45355
|
-
|
|
47281
|
+
const balance = await this.tokenDomainService.getSpecificTokenBalance({
|
|
45356
47282
|
accountAddress: request.accountAddress || '',
|
|
45357
47283
|
contractAddress: request.contractAddress,
|
|
45358
47284
|
abi: request.abi,
|
|
45359
47285
|
tokenId: request.tokenId || '',
|
|
45360
47286
|
chainId: request.chainId
|
|
45361
47287
|
});
|
|
47288
|
+
return localizeTokenBalance(balance, this.getLocale());
|
|
45362
47289
|
}
|
|
45363
47290
|
/**
|
|
45364
47291
|
* Get metadata for a specific token from on-chain.
|
|
@@ -45378,14 +47305,15 @@ class Web3ApplicationService {
|
|
|
45378
47305
|
tokenId: request.tokenId || '',
|
|
45379
47306
|
chainId: request.chainId
|
|
45380
47307
|
});
|
|
45381
|
-
// Metadata is already normalized at infrastructure layer
|
|
45382
|
-
|
|
47308
|
+
// Metadata is already normalized at infrastructure layer; the cache holds the raw JSON,
|
|
47309
|
+
// so the language is applied here, at read time
|
|
47310
|
+
return domainResult.metadata ? localizeTokenMetadata(domainResult.metadata, this.getLocale()) : null;
|
|
45383
47311
|
}
|
|
45384
47312
|
/**
|
|
45385
47313
|
* Retrieve entire collection of tokens with balance and metadata
|
|
45386
47314
|
*/
|
|
45387
47315
|
async getTokenCollection(request) {
|
|
45388
|
-
|
|
47316
|
+
const collection = await this.tokenDomainService.getTokenCollection({
|
|
45389
47317
|
accountAddress: request.accountAddress || '',
|
|
45390
47318
|
contractAddress: request.contractAddress,
|
|
45391
47319
|
abi: request.abi,
|
|
@@ -45393,6 +47321,7 @@ class Web3ApplicationService {
|
|
|
45393
47321
|
maxTokens: request.maxTokens,
|
|
45394
47322
|
tokenIds: request.tokenIds
|
|
45395
47323
|
});
|
|
47324
|
+
return localizeTokenCollection(collection, this.getLocale());
|
|
45396
47325
|
}
|
|
45397
47326
|
/**
|
|
45398
47327
|
* Resolve IPFS URLs to HTTPS if needed.
|
|
@@ -45400,7 +47329,7 @@ class Web3ApplicationService {
|
|
|
45400
47329
|
* IPFS gateway is resolved from tenant configuration (chain-agnostic).
|
|
45401
47330
|
*/
|
|
45402
47331
|
async resolveIPFSUrl(url) {
|
|
45403
|
-
return this.
|
|
47332
|
+
return this.ipfsApi.resolveIPFSUrl(url);
|
|
45404
47333
|
}
|
|
45405
47334
|
/**
|
|
45406
47335
|
* Get IPFS gateway domain from tenant configuration.
|
|
@@ -45409,7 +47338,7 @@ class Web3ApplicationService {
|
|
|
45409
47338
|
* @returns Gateway domain (e.g., 'pers.mypinata.cloud')
|
|
45410
47339
|
*/
|
|
45411
47340
|
async getIpfsGatewayDomain() {
|
|
45412
|
-
return this.
|
|
47341
|
+
return this.ipfsApi.getIpfsGatewayDomain();
|
|
45413
47342
|
}
|
|
45414
47343
|
/**
|
|
45415
47344
|
* Fetch and process metadata from any URI with IPFS conversion.
|
|
@@ -45419,7 +47348,8 @@ class Web3ApplicationService {
|
|
|
45419
47348
|
* IPFS gateway is resolved from tenant configuration (chain-agnostic).
|
|
45420
47349
|
*/
|
|
45421
47350
|
async fetchAndProcessMetadata(tokenUri) {
|
|
45422
|
-
|
|
47351
|
+
const metadata = await this.ipfsApi.fetchAndProcessMetadata(tokenUri);
|
|
47352
|
+
return metadata ? localizeTokenMetadata(metadata, this.getLocale()) : null;
|
|
45423
47353
|
}
|
|
45424
47354
|
}
|
|
45425
47355
|
|
|
@@ -45496,109 +47426,37 @@ class Web3InfrastructureApi {
|
|
|
45496
47426
|
}
|
|
45497
47427
|
|
|
45498
47428
|
/**
|
|
45499
|
-
* IPFSInfrastructureApi -
|
|
45500
|
-
*
|
|
45501
|
-
*
|
|
45502
|
-
* IMPORTANT: IPFS gateway domain is now fetched from tenant configuration (TenantClientConfigDTO),
|
|
45503
|
-
* not from chain data. This is the correct architecture since IPFS is chain-agnostic.
|
|
47429
|
+
* IPFSInfrastructureApi - Web3-side adapter over the shared IPFS and token metadata resolvers.
|
|
47430
|
+
* Pass the SDK's shared `sources` to reuse its resolvers; otherwise they are built from `config`.
|
|
47431
|
+
* Results are raw (language-agnostic); the locale is applied where data is handed out.
|
|
45504
47432
|
*/
|
|
45505
47433
|
class IPFSInfrastructureApi {
|
|
45506
|
-
constructor(
|
|
45507
|
-
this.
|
|
45508
|
-
this.
|
|
47434
|
+
constructor(config, sources = createIpfsSources(config)) {
|
|
47435
|
+
this.resolver = sources.resolver;
|
|
47436
|
+
this.metadata = sources.metadata;
|
|
45509
47437
|
}
|
|
45510
|
-
|
|
45511
|
-
|
|
45512
|
-
*
|
|
45513
|
-
* @returns The IPFS gateway domain from tenant configuration
|
|
45514
|
-
* @throws Error if tenant config is not found or ipfsGatewayDomain is not configured
|
|
45515
|
-
*/
|
|
45516
|
-
async getIpfsGatewayDomain() {
|
|
45517
|
-
const cacheKey = 'gateway';
|
|
45518
|
-
return this.cache.getOrSet(cacheKey, async () => {
|
|
45519
|
-
const clientConfig = await this.tenantManager.getClientConfig();
|
|
45520
|
-
if (!clientConfig.ipfsGatewayDomain) {
|
|
45521
|
-
throw new Error(`IPFS gateway domain not configured for tenant. ` +
|
|
45522
|
-
`Please configure ipfsGatewayDomain in the tenant configuration.`);
|
|
45523
|
-
}
|
|
45524
|
-
return clientConfig.ipfsGatewayDomain;
|
|
45525
|
-
}, CacheTTL.GATEWAY);
|
|
47438
|
+
getIpfsGatewayDomain() {
|
|
47439
|
+
return this.resolver.getGatewayDomain();
|
|
45526
47440
|
}
|
|
45527
|
-
|
|
45528
|
-
|
|
45529
|
-
*
|
|
45530
|
-
* @param url - The URL to resolve (can be ipfs:// or https://)
|
|
45531
|
-
* @returns Resolved HTTPS URL
|
|
45532
|
-
*/
|
|
45533
|
-
async resolveIPFSUrl(url) {
|
|
45534
|
-
if (url.startsWith('ipfs://')) {
|
|
45535
|
-
const gateway = await this.getIpfsGatewayDomain();
|
|
45536
|
-
return url.replace('ipfs://', `https://${gateway}/ipfs/`);
|
|
45537
|
-
}
|
|
45538
|
-
return url;
|
|
47441
|
+
resolveIPFSUrl(url) {
|
|
47442
|
+
return this.resolver.resolveUrl(url);
|
|
45539
47443
|
}
|
|
45540
|
-
|
|
45541
|
-
|
|
45542
|
-
*
|
|
45543
|
-
* NORMALIZATION BOUNDARY: This is the single place where ERC metadata standard
|
|
45544
|
-
* (snake_case: image, animation_url) is converted to SDK format (camelCase: imageUrl, animationUrl).
|
|
45545
|
-
* All downstream consumers receive normalized SDK format with resolved HTTPS URLs.
|
|
45546
|
-
*
|
|
45547
|
-
* @param tokenUri - Token URI (can be ipfs:// or https://)
|
|
45548
|
-
* @returns Normalized TokenMetadata with resolved HTTPS URLs, or null on error
|
|
45549
|
-
*/
|
|
45550
|
-
async fetchAndProcessMetadata(tokenUri) {
|
|
45551
|
-
try {
|
|
45552
|
-
const resolvedUri = await this.resolveIPFSUrl(tokenUri);
|
|
45553
|
-
const response = await fetch(resolvedUri);
|
|
45554
|
-
if (!response.ok) {
|
|
45555
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
45556
|
-
}
|
|
45557
|
-
const rawMetadata = await response.json();
|
|
45558
|
-
// Resolve any IPFS URLs in string fields
|
|
45559
|
-
for (const key of Object.keys(rawMetadata)) {
|
|
45560
|
-
const value = rawMetadata[key];
|
|
45561
|
-
if (typeof value === 'string' && value.startsWith('ipfs://')) {
|
|
45562
|
-
rawMetadata[key] = await this.resolveIPFSUrl(value);
|
|
45563
|
-
}
|
|
45564
|
-
}
|
|
45565
|
-
// Extract custom properties (anything not in ERC standard)
|
|
45566
|
-
const customProperties = Object.fromEntries(Object.entries(rawMetadata).filter(([key]) => !['name', 'description', 'image', 'animation_url', 'external_url', 'attributes'].includes(key)));
|
|
45567
|
-
// Return normalized SDK format
|
|
45568
|
-
const result = {
|
|
45569
|
-
name: String(rawMetadata.name ?? ''),
|
|
45570
|
-
description: String(rawMetadata.description ?? ''),
|
|
45571
|
-
imageUrl: String(rawMetadata.image ?? ''),
|
|
45572
|
-
animationUrl: rawMetadata.animation_url ? String(rawMetadata.animation_url) : undefined,
|
|
45573
|
-
externalUrl: rawMetadata.external_url ? String(rawMetadata.external_url) : undefined,
|
|
45574
|
-
attributes: Array.isArray(rawMetadata.attributes) ? rawMetadata.attributes : [],
|
|
45575
|
-
...customProperties
|
|
45576
|
-
};
|
|
45577
|
-
return result;
|
|
45578
|
-
}
|
|
45579
|
-
catch (error) {
|
|
45580
|
-
console.error('Error fetching metadata:', error);
|
|
45581
|
-
return null;
|
|
45582
|
-
}
|
|
47444
|
+
fetchAndProcessMetadata(tokenUri) {
|
|
47445
|
+
return this.metadata.fetch(tokenUri);
|
|
45583
47446
|
}
|
|
45584
47447
|
async fetchFromUrl(url) {
|
|
45585
47448
|
try {
|
|
45586
|
-
|
|
45587
|
-
if (!response.ok) {
|
|
45588
|
-
throw new Error(`Failed to fetch from ${url}: ${response.statusText}`);
|
|
45589
|
-
}
|
|
45590
|
-
return await response.json();
|
|
47449
|
+
return await this.resolver.fetchJson(url);
|
|
45591
47450
|
}
|
|
45592
47451
|
catch (error) {
|
|
45593
47452
|
console.error(`Error fetching from URL ${url}:`, error);
|
|
45594
47453
|
throw error;
|
|
45595
47454
|
}
|
|
45596
47455
|
}
|
|
45597
|
-
/**
|
|
45598
|
-
* Clear IPFS cache for this service
|
|
45599
|
-
*/
|
|
47456
|
+
/** Clear cached gateway and metadata entries */
|
|
45600
47457
|
clearCache() {
|
|
45601
|
-
this.
|
|
47458
|
+
this.resolver.clearCache();
|
|
47459
|
+
this.metadata.clearCache();
|
|
45602
47460
|
}
|
|
45603
47461
|
}
|
|
45604
47462
|
|
|
@@ -45715,12 +47573,11 @@ class Web3Manager {
|
|
|
45715
47573
|
this.web3ChainService = new Web3ChainService(web3ChainApi);
|
|
45716
47574
|
// Initialize Web3 Application service with TenantManager for IPFS
|
|
45717
47575
|
const web3InfrastructureApi = new Web3InfrastructureApi(this.web3ChainService);
|
|
45718
|
-
const ipfsInfrastructureApi = new IPFSInfrastructureApi(this.tenantManager);
|
|
45719
|
-
this.web3ApplicationService = new Web3ApplicationService(web3InfrastructureApi, ipfsInfrastructureApi);
|
|
47576
|
+
const ipfsInfrastructureApi = new IPFSInfrastructureApi(this.tenantManager, this.tenantManager.ipfs.sources);
|
|
47577
|
+
this.web3ApplicationService = new Web3ApplicationService(web3InfrastructureApi, ipfsInfrastructureApi, () => apiClient.getLocale());
|
|
45720
47578
|
// Initialize domain services for managers
|
|
45721
47579
|
const contractDomainService = new ContractDomainService();
|
|
45722
|
-
|
|
45723
|
-
this.tokenDomainService = new TokenDomainService(web3InfrastructureApi, metadataDomainService, contractDomainService);
|
|
47580
|
+
this.tokenDomainService = new TokenDomainService(web3InfrastructureApi, ipfsInfrastructureApi, contractDomainService);
|
|
45724
47581
|
}
|
|
45725
47582
|
/**
|
|
45726
47583
|
* Get the TokenCollectionManager for NFT collection operations.
|
|
@@ -45748,7 +47605,7 @@ class Web3Manager {
|
|
|
45748
47605
|
*/
|
|
45749
47606
|
get collectionManager() {
|
|
45750
47607
|
if (!this._collectionManager) {
|
|
45751
|
-
this._collectionManager = new TokenCollectionManager(this.tokenDomainService, this.eventEmitter);
|
|
47608
|
+
this._collectionManager = new TokenCollectionManager(this.tokenDomainService, this.eventEmitter, { getLocale: () => this.apiClient.getLocale() });
|
|
45752
47609
|
}
|
|
45753
47610
|
return this._collectionManager;
|
|
45754
47611
|
}
|
|
@@ -45819,10 +47676,10 @@ class Web3Manager {
|
|
|
45819
47676
|
*
|
|
45820
47677
|
* @param tokenUri - Token URI to fetch metadata from
|
|
45821
47678
|
* @returns Promise resolving to processed metadata or null if not found
|
|
45822
|
-
* @deprecated Use sdk.
|
|
47679
|
+
* @deprecated Use sdk.ipfs.fetchMetadata() instead - IPFS is chain-agnostic
|
|
45823
47680
|
*/
|
|
45824
47681
|
async fetchAndProcessMetadata(tokenUri) {
|
|
45825
|
-
return this.
|
|
47682
|
+
return this.web3ApplicationService.fetchAndProcessMetadata(tokenUri);
|
|
45826
47683
|
}
|
|
45827
47684
|
/**
|
|
45828
47685
|
* Get blockchain chain data by chain ID
|
|
@@ -46145,7 +48002,7 @@ const useWeb3 = () => {
|
|
|
46145
48002
|
* Converts an ipfs:// URL to an HTTPS URL using the tenant's configured IPFS gateway.
|
|
46146
48003
|
* Non-IPFS URLs are returned unchanged.
|
|
46147
48004
|
*
|
|
46148
|
-
* Note: Uses sdk.
|
|
48005
|
+
* Note: Uses sdk.ipfs for IPFS resolution (chain-agnostic).
|
|
46149
48006
|
*
|
|
46150
48007
|
* @param url - IPFS URL to resolve (ipfs://...)
|
|
46151
48008
|
* @returns Promise resolving to HTTP gateway URL
|
|
@@ -46162,7 +48019,7 @@ const useWeb3 = () => {
|
|
|
46162
48019
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
46163
48020
|
}
|
|
46164
48021
|
try {
|
|
46165
|
-
const result = await sdk.
|
|
48022
|
+
const result = await sdk.ipfs.resolveUrl(url);
|
|
46166
48023
|
return result;
|
|
46167
48024
|
}
|
|
46168
48025
|
catch (error) {
|
|
@@ -46173,7 +48030,7 @@ const useWeb3 = () => {
|
|
|
46173
48030
|
/**
|
|
46174
48031
|
* Fetch and process token metadata from a URI
|
|
46175
48032
|
*
|
|
46176
|
-
* Note: Uses sdk.
|
|
48033
|
+
* Note: Uses sdk.ipfs for metadata fetching (chain-agnostic IPFS).
|
|
46177
48034
|
*
|
|
46178
48035
|
* @param tokenUri - Token URI to fetch metadata from
|
|
46179
48036
|
* @returns Promise resolving to processed metadata or null
|
|
@@ -46183,7 +48040,7 @@ const useWeb3 = () => {
|
|
|
46183
48040
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
46184
48041
|
}
|
|
46185
48042
|
try {
|
|
46186
|
-
const result = await sdk.
|
|
48043
|
+
const result = await sdk.ipfs.fetchMetadata(tokenUri);
|
|
46187
48044
|
return result;
|
|
46188
48045
|
}
|
|
46189
48046
|
catch (error) {
|
|
@@ -46810,7 +48667,7 @@ class AngularHttpClientAdapter {
|
|
|
46810
48667
|
}
|
|
46811
48668
|
async convertObservableToPromise(observable) {
|
|
46812
48669
|
// Dynamically import firstValueFrom to avoid build-time dependency
|
|
46813
|
-
const { firstValueFrom } = await
|
|
48670
|
+
const { firstValueFrom } = await import('rxjs');
|
|
46814
48671
|
return firstValueFrom(observable);
|
|
46815
48672
|
}
|
|
46816
48673
|
mergeOptions(options) {
|
|
@@ -46876,7 +48733,10 @@ class BrowserFetchClientAdapter {
|
|
|
46876
48733
|
}
|
|
46877
48734
|
const response = await fetch(url, fetchOptions);
|
|
46878
48735
|
if (!response.ok) {
|
|
46879
|
-
|
|
48736
|
+
const httpError = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
48737
|
+
httpError.status = response.status;
|
|
48738
|
+
httpError.statusText = response.statusText;
|
|
48739
|
+
throw httpError;
|
|
46880
48740
|
}
|
|
46881
48741
|
// Handle different response types
|
|
46882
48742
|
const contentType = response.headers.get('content-type');
|
|
@@ -47848,15 +49708,22 @@ const useTransactions = () => {
|
|
|
47848
49708
|
}
|
|
47849
49709
|
try {
|
|
47850
49710
|
const result = await sdk.transactions.createTransaction(request);
|
|
47851
|
-
//
|
|
49711
|
+
// If backend already signed custodially, return as-is so caller can build QR
|
|
49712
|
+
// from result.signatureData directly (no client signing needed)
|
|
49713
|
+
if (needsSubmission(result)) {
|
|
49714
|
+
return result;
|
|
49715
|
+
}
|
|
49716
|
+
// If client signer is unavailable, return as-is so caller can fall back to
|
|
49717
|
+
// result.actionable.actionUrl (external signing URL) if present
|
|
49718
|
+
if (!isSignerAvailable) {
|
|
49719
|
+
console.warn('[useTransactions] Transaction signer not available, returning result for caller to handle');
|
|
49720
|
+
return result;
|
|
49721
|
+
}
|
|
49722
|
+
// Check if transaction requires client signing (contains actionable authToken)
|
|
47852
49723
|
// Type assertion needed as TransactionRequestResponseDTO type may not include all dynamic properties
|
|
47853
49724
|
const txToken = result?.actionable?.authToken;
|
|
47854
49725
|
if (txToken) {
|
|
47855
49726
|
try {
|
|
47856
|
-
if (!isSignerAvailable) {
|
|
47857
|
-
console.warn('[useTransactions] Transaction signer not available, skipping automatic signing');
|
|
47858
|
-
return result;
|
|
47859
|
-
}
|
|
47860
49727
|
// Automatically sign the transaction using the authToken
|
|
47861
49728
|
const signingResult = await signAndSubmitTransactionWithJWT(txToken, onStatusUpdate);
|
|
47862
49729
|
if (signingResult.success) {
|
|
@@ -48212,12 +50079,12 @@ const useBusiness = () => {
|
|
|
48212
50079
|
* console.log('Business created:', business);
|
|
48213
50080
|
* ```
|
|
48214
50081
|
*/
|
|
48215
|
-
const createBusiness = react.useCallback(async (
|
|
50082
|
+
const createBusiness = react.useCallback(async (dto) => {
|
|
48216
50083
|
if (!isInitialized || !sdk) {
|
|
48217
50084
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
48218
50085
|
}
|
|
48219
50086
|
try {
|
|
48220
|
-
const result = await sdk.businesses.createBusiness(
|
|
50087
|
+
const result = await sdk.businesses.createBusiness(dto);
|
|
48221
50088
|
return result;
|
|
48222
50089
|
}
|
|
48223
50090
|
catch (error) {
|
|
@@ -49076,7 +50943,7 @@ const useRedemptions = () => {
|
|
|
49076
50943
|
* Automatically handles IPFS URLs by resolving them to HTTPS via tenant's gateway.
|
|
49077
50944
|
* Non-IPFS URLs are returned as-is. Handles loading and error states.
|
|
49078
50945
|
*
|
|
49079
|
-
* Uses sdk.
|
|
50946
|
+
* Uses sdk.ipfs for IPFS resolution (lightweight, no Web3 deps).
|
|
49080
50947
|
*
|
|
49081
50948
|
* @param url - The URL to resolve (can be ipfs://, https://, or undefined)
|
|
49082
50949
|
* @returns Object with resolved URL, loading state, and error
|
|
@@ -49108,7 +50975,7 @@ function useResolvedUrl(url) {
|
|
|
49108
50975
|
}
|
|
49109
50976
|
setIsLoading(true);
|
|
49110
50977
|
setError(null);
|
|
49111
|
-
sdk.
|
|
50978
|
+
sdk.ipfs.resolveUrl(url)
|
|
49112
50979
|
.then((resolved) => {
|
|
49113
50980
|
setResolvedUrl(resolved);
|
|
49114
50981
|
})
|
|
@@ -49135,7 +51002,7 @@ function useResolvedUrl(url) {
|
|
|
49135
51002
|
* Handles IPFS resolution automatically and returns parsed metadata
|
|
49136
51003
|
* with all nested IPFS URLs already resolved to HTTPS.
|
|
49137
51004
|
*
|
|
49138
|
-
* Uses sdk.
|
|
51005
|
+
* Uses sdk.ipfs for metadata fetching (lightweight, no Web3 deps).
|
|
49139
51006
|
*
|
|
49140
51007
|
* @param metadataUri - The metadata URI to fetch (can be ipfs:// or https://)
|
|
49141
51008
|
* @returns Object with metadata, loading state, error, and refresh function
|
|
@@ -49160,7 +51027,7 @@ function useResolvedUrl(url) {
|
|
|
49160
51027
|
* ```
|
|
49161
51028
|
*/
|
|
49162
51029
|
function useMetadata(metadataUri) {
|
|
49163
|
-
const { sdk, isInitialized } = usePersSDK();
|
|
51030
|
+
const { sdk, isInitialized, locale } = usePersSDK();
|
|
49164
51031
|
const [metadata, setMetadata] = react.useState(null);
|
|
49165
51032
|
const [isLoading, setIsLoading] = react.useState(false);
|
|
49166
51033
|
const [error, setError] = react.useState(null);
|
|
@@ -49172,7 +51039,7 @@ function useMetadata(metadataUri) {
|
|
|
49172
51039
|
setIsLoading(true);
|
|
49173
51040
|
setError(null);
|
|
49174
51041
|
try {
|
|
49175
|
-
const result = await sdk.
|
|
51042
|
+
const result = await sdk.ipfs.fetchMetadata(metadataUri);
|
|
49176
51043
|
setMetadata(result);
|
|
49177
51044
|
}
|
|
49178
51045
|
catch (err) {
|
|
@@ -49182,12 +51049,12 @@ function useMetadata(metadataUri) {
|
|
|
49182
51049
|
finally {
|
|
49183
51050
|
setIsLoading(false);
|
|
49184
51051
|
}
|
|
49185
|
-
}, [metadataUri, isInitialized, sdk]);
|
|
51052
|
+
}, [metadataUri, isInitialized, sdk, locale]);
|
|
49186
51053
|
react.useEffect(() => {
|
|
49187
51054
|
fetchMetadata();
|
|
49188
51055
|
}, [fetchMetadata]);
|
|
49189
51056
|
return {
|
|
49190
|
-
/** Parsed metadata with resolved IPFS URLs (imageUrl, animationUrl, etc.) */
|
|
51057
|
+
/** Parsed metadata with resolved IPFS URLs (imageUrl, animationUrl, etc.), localized for the current locale */
|
|
49191
51058
|
metadata,
|
|
49192
51059
|
/** Whether fetch is in progress */
|
|
49193
51060
|
isLoading,
|
|
@@ -50816,6 +52683,8 @@ exports.AuthManager = AuthManager;
|
|
|
50816
52683
|
exports.AuthService = AuthService;
|
|
50817
52684
|
exports.AuthTokenManager = AuthTokenManager;
|
|
50818
52685
|
exports.AuthenticationError = AuthenticationError;
|
|
52686
|
+
exports.BUSINESS_TRANSLATABLE_FIELDS = BUSINESS_TRANSLATABLE_FIELDS;
|
|
52687
|
+
exports.BUSINESS_TYPE_TRANSLATABLE_FIELDS = BUSINESS_TYPE_TRANSLATABLE_FIELDS;
|
|
50819
52688
|
exports.BaseTokenService = BaseTokenService;
|
|
50820
52689
|
exports.BookingApi = BookingApi;
|
|
50821
52690
|
exports.BookingManager = BookingManager;
|
|
@@ -50826,6 +52695,11 @@ exports.BusinessManager = BusinessManager;
|
|
|
50826
52695
|
exports.BusinessMembershipApi = BusinessMembershipApi;
|
|
50827
52696
|
exports.BusinessMembershipService = BusinessMembershipService;
|
|
50828
52697
|
exports.BusinessService = BusinessService;
|
|
52698
|
+
exports.CAMPAIGN_TRANSLATABLE_FIELDS = CAMPAIGN_TRANSLATABLE_FIELDS;
|
|
52699
|
+
exports.CAMPAIGN_TRIGGER_TRANSLATABLE_FIELDS = CAMPAIGN_TRIGGER_TRANSLATABLE_FIELDS;
|
|
52700
|
+
exports.CHAIN_READ_PERMISSIONS = CHAIN_READ_PERMISSIONS;
|
|
52701
|
+
exports.CHAIN_WRITE_PERMISSIONS = CHAIN_WRITE_PERMISSIONS;
|
|
52702
|
+
exports.CUSTOM_FIELD_TRANSLATABLE_FIELDS = CUSTOM_FIELD_TRANSLATABLE_FIELDS;
|
|
50829
52703
|
exports.CacheService = CacheService;
|
|
50830
52704
|
exports.CacheTTL = CacheTTL;
|
|
50831
52705
|
exports.CampaignApi = CampaignApi;
|
|
@@ -50837,6 +52711,8 @@ exports.CommonErrorCodes = CommonErrorCodes;
|
|
|
50837
52711
|
exports.CustomFieldDefinitionManager = CustomFieldDefinitionManager;
|
|
50838
52712
|
exports.DATA_SOURCE_CHANNELS = DATA_SOURCE_CHANNELS;
|
|
50839
52713
|
exports.DEFAULT_DATA_SOURCE = DEFAULT_DATA_SOURCE;
|
|
52714
|
+
exports.DEFAULT_PAGE = DEFAULT_PAGE;
|
|
52715
|
+
exports.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;
|
|
50840
52716
|
exports.DEFAULT_PERS_CONFIG = DEFAULT_PERS_CONFIG;
|
|
50841
52717
|
exports.DPOP_STORAGE_KEYS = DPOP_STORAGE_KEYS;
|
|
50842
52718
|
exports.DPoPManager = DPoPManager;
|
|
@@ -50851,6 +52727,7 @@ exports.ENGAGEMENT_RATE_FORMULA = ENGAGEMENT_RATE_FORMULA;
|
|
|
50851
52727
|
exports.EntityTypes = EntityTypes;
|
|
50852
52728
|
exports.ErrorDomains = ErrorDomains;
|
|
50853
52729
|
exports.ErrorUtils = ErrorUtils;
|
|
52730
|
+
exports.ExternalEventType = ExternalEventType;
|
|
50854
52731
|
exports.FATAL_AUTH_CODES = FATAL_AUTH_CODES;
|
|
50855
52732
|
exports.FileApi = FileApi;
|
|
50856
52733
|
exports.FileManager = FileManager;
|
|
@@ -50864,16 +52741,24 @@ exports.IMAGE_CDN_MAX_INPUT_SIZE = IMAGE_CDN_MAX_INPUT_SIZE;
|
|
|
50864
52741
|
exports.IMAGE_CDN_PRESETS = IMAGE_CDN_PRESETS;
|
|
50865
52742
|
exports.IMAGE_CDN_SUPPORTED_FORMATS = IMAGE_CDN_SUPPORTED_FORMATS;
|
|
50866
52743
|
exports.IndexedDBTokenStorage = IndexedDBTokenStorage;
|
|
52744
|
+
exports.IpfsManager = IpfsManager;
|
|
50867
52745
|
exports.KNOWN_SOURCE_DOMAINS = KNOWN_SOURCE_DOMAINS;
|
|
52746
|
+
exports.LOCALE_HEADER = LOCALE_HEADER;
|
|
52747
|
+
exports.LOCALE_MAX_LENGTH = LOCALE_MAX_LENGTH;
|
|
50868
52748
|
exports.LOCATION_TYPES = LOCATION_TYPES;
|
|
50869
52749
|
exports.LocalStorageTokenStorage = LocalStorageTokenStorage;
|
|
52750
|
+
exports.LocaleManager = LocaleManager;
|
|
50870
52751
|
exports.LogoutRequired = LogoutRequired;
|
|
50871
52752
|
exports.MEMBERSHIP_ROLE_HIERARCHY = MEMBERSHIP_ROLE_HIERARCHY;
|
|
50872
52753
|
exports.MemoryTokenStorage = MemoryTokenStorage;
|
|
50873
52754
|
exports.NativeTokenTypes = NativeTokenTypes;
|
|
50874
52755
|
exports.NetworkError = NetworkError;
|
|
50875
52756
|
exports.NodeHttpClientAdapter = NodeHttpClientAdapter;
|
|
52757
|
+
exports.NotificationApi = NotificationApi;
|
|
52758
|
+
exports.NotificationManager = NotificationManager;
|
|
52759
|
+
exports.NotificationService = NotificationService;
|
|
50876
52760
|
exports.OttAuthMethod = OttAuthMethod;
|
|
52761
|
+
exports.PURCHASE_TOKEN_TRANSLATABLE_FIELDS = PURCHASE_TOKEN_TRANSLATABLE_FIELDS;
|
|
50877
52762
|
exports.PaymentApi = PurchaseApi;
|
|
50878
52763
|
exports.PaymentService = PaymentService;
|
|
50879
52764
|
exports.PersApiClient = PersApiClient;
|
|
@@ -50885,12 +52770,18 @@ exports.PersSDKProvider = PersSDKProvider;
|
|
|
50885
52770
|
exports.ProviderTokenRefreshNeeded = ProviderTokenRefreshNeeded;
|
|
50886
52771
|
exports.PurchaseManager = PurchaseManager;
|
|
50887
52772
|
exports.QUERY_OPERATOR_SQL_MAP = QUERY_OPERATOR_SQL_MAP;
|
|
52773
|
+
exports.REDEMPTION_ELIGIBILITY_GATES = REDEMPTION_ELIGIBILITY_GATES;
|
|
52774
|
+
exports.REDEMPTION_TRANSLATABLE_FIELDS = REDEMPTION_TRANSLATABLE_FIELDS;
|
|
52775
|
+
exports.REDEMPTION_TYPE_TRANSLATABLE_FIELDS = REDEMPTION_TYPE_TRANSLATABLE_FIELDS;
|
|
50888
52776
|
exports.ReactNativeDPoPProvider = ReactNativeDPoPProvider;
|
|
50889
52777
|
exports.ReactNativeHttpClient = ReactNativeHttpClient;
|
|
50890
52778
|
exports.ReactNativeSecureStorage = ReactNativeSecureStorage;
|
|
50891
52779
|
exports.RedemptionApi = RedemptionApi;
|
|
50892
52780
|
exports.RedemptionManager = RedemptionManager;
|
|
50893
52781
|
exports.RedemptionService = RedemptionService;
|
|
52782
|
+
exports.RedirectApi = RedirectApi;
|
|
52783
|
+
exports.RedirectManager = RedirectManager;
|
|
52784
|
+
exports.RedirectService = RedirectService;
|
|
50894
52785
|
exports.SDK_NAME = SDK_NAME;
|
|
50895
52786
|
exports.SDK_USER_AGENT = SDK_USER_AGENT;
|
|
50896
52787
|
exports.SDK_VERSION = SDK_VERSION;
|
|
@@ -50899,9 +52790,16 @@ exports.SOURCE_LOGIC_TYPES = SOURCE_LOGIC_TYPES;
|
|
|
50899
52790
|
exports.SOURCE_LOGIC_TYPE_VALUES = SOURCE_LOGIC_TYPE_VALUES;
|
|
50900
52791
|
exports.SdkErrorCodes = SdkErrorCodes;
|
|
50901
52792
|
exports.StaticJwtAuthProvider = StaticJwtAuthProvider;
|
|
52793
|
+
exports.TENANT_TRANSLATABLE_FIELDS = TENANT_TRANSLATABLE_FIELDS;
|
|
52794
|
+
exports.TOKEN_METADATA_TRANSLATABLE_FIELDS = TOKEN_METADATA_TRANSLATABLE_FIELDS;
|
|
50902
52795
|
exports.TOKEN_VALIDITY_TYPE_VALUES = TOKEN_VALIDITY_TYPE_VALUES;
|
|
50903
52796
|
exports.TRANSACTION_FORMATS = TRANSACTION_FORMATS;
|
|
50904
52797
|
exports.TRANSACTION_FORMAT_DESCRIPTIONS = TRANSACTION_FORMAT_DESCRIPTIONS;
|
|
52798
|
+
exports.TRANSLATIONS_MAX_LOCALES = TRANSLATIONS_MAX_LOCALES;
|
|
52799
|
+
exports.TRANSLATIONS_MAX_SIZE = TRANSLATIONS_MAX_SIZE;
|
|
52800
|
+
exports.TRANSLATION_FIELD_MAX_LENGTH = TRANSLATION_FIELD_MAX_LENGTH;
|
|
52801
|
+
exports.TRANSLATION_VALUE_MAX_LENGTH = TRANSLATION_VALUE_MAX_LENGTH;
|
|
52802
|
+
exports.TRIGGER_SOURCE_TRANSLATABLE_FIELDS = TRIGGER_SOURCE_TRANSLATABLE_FIELDS;
|
|
50905
52803
|
exports.TRIGGER_SOURCE_TYPES = TRIGGER_SOURCE_TYPES;
|
|
50906
52804
|
exports.TRIGGER_SOURCE_TYPE_VALUES = TRIGGER_SOURCE_TYPE_VALUES;
|
|
50907
52805
|
exports.TenantApi = TenantApi;
|
|
@@ -50921,6 +52819,7 @@ exports.TriggerSourceApi = TriggerSourceApi;
|
|
|
50921
52819
|
exports.TriggerSourceManager = TriggerSourceManager;
|
|
50922
52820
|
exports.TriggerSourceService = TriggerSourceService;
|
|
50923
52821
|
exports.URL_REGEX = URL_REGEX;
|
|
52822
|
+
exports.USER_STATUS_TYPE_TRANSLATABLE_FIELDS = USER_STATUS_TYPE_TRANSLATABLE_FIELDS;
|
|
50924
52823
|
exports.UUID_REGEX = UUID_REGEX;
|
|
50925
52824
|
exports.UserApi = UserApi;
|
|
50926
52825
|
exports.UserManager = UserManager;
|
|
@@ -50929,6 +52828,7 @@ exports.UserStatusApi = UserStatusApi;
|
|
|
50929
52828
|
exports.UserStatusManager = UserStatusManager;
|
|
50930
52829
|
exports.UserStatusService = UserStatusService;
|
|
50931
52830
|
exports.VALID_BUSINESS_MEMBERSHIP_RELATIONS = VALID_BUSINESS_MEMBERSHIP_RELATIONS;
|
|
52831
|
+
exports.VALID_BUSINESS_RELATIONS = VALID_BUSINESS_RELATIONS;
|
|
50932
52832
|
exports.VALID_CAMPAIGN_CLAIM_RELATIONS = VALID_CAMPAIGN_CLAIM_RELATIONS;
|
|
50933
52833
|
exports.VALID_CAMPAIGN_RELATIONS = VALID_CAMPAIGN_RELATIONS;
|
|
50934
52834
|
exports.VALID_REDEMPTION_REDEEM_RELATIONS = VALID_REDEMPTION_REDEEM_RELATIONS;
|
|
@@ -50944,6 +52844,7 @@ exports.WebhookApi = WebhookApi;
|
|
|
50944
52844
|
exports.WebhookManager = WebhookManager;
|
|
50945
52845
|
exports.WebhookService = WebhookService;
|
|
50946
52846
|
exports.apiPublicKeyTestPrefix = apiPublicKeyTestPrefix;
|
|
52847
|
+
exports.assertAvailable = assertAvailable;
|
|
50947
52848
|
exports.buildApiRoot = buildApiRoot;
|
|
50948
52849
|
exports.buildBurnRequest = buildBurnRequest;
|
|
50949
52850
|
exports.buildImageCdnUrl = buildImageCdnUrl;
|
|
@@ -50955,9 +52856,12 @@ exports.buildPaginationParams = buildPaginationParams;
|
|
|
50955
52856
|
exports.buildPendingTransactionData = buildPendingTransactionData;
|
|
50956
52857
|
exports.buildSubmissionRequest = buildSubmissionRequest;
|
|
50957
52858
|
exports.buildTransferRequest = buildTransferRequest;
|
|
52859
|
+
exports.buildTranslationsPatch = buildTranslationsPatch;
|
|
50958
52860
|
exports.buildWalletEventsWsUrl = buildWalletEventsWsUrl;
|
|
50959
52861
|
exports.builtInUserFieldMetadata = builtInUserFieldMetadata;
|
|
50960
52862
|
exports.builtInUserFields = builtInUserFields;
|
|
52863
|
+
exports.canManageCampaignTriggerSources = canManageCampaignTriggerSources;
|
|
52864
|
+
exports.cleanTranslations = cleanTranslations;
|
|
50961
52865
|
exports.compareValues = compareValues;
|
|
50962
52866
|
exports.computeBookingStatus = computeBookingStatus;
|
|
50963
52867
|
exports.createPersEventsClient = createPersEventsClient;
|
|
@@ -50967,18 +52871,21 @@ exports.createUserIdentifierObject = createUserIdentifierObject;
|
|
|
50967
52871
|
exports.createUserStatusSDK = createUserStatusSDK;
|
|
50968
52872
|
exports.customFieldEntityTypes = customFieldEntityTypes;
|
|
50969
52873
|
exports.detectChannelFromUserAgent = detectChannelFromUserAgent;
|
|
52874
|
+
exports.detectDeviceLocale = detectDeviceLocale;
|
|
50970
52875
|
exports.detectEnvironment = detectEnvironment;
|
|
50971
52876
|
exports.detectIdentifierType = detectIdentifierType;
|
|
50972
52877
|
exports.detectReactNativePlatform = detectReactNativePlatform;
|
|
50973
52878
|
exports.detectSourceFromReferrer = detectSourceFromReferrer;
|
|
50974
52879
|
exports.detectWebPlatform = detectWebPlatform;
|
|
50975
52880
|
exports.environment = environment;
|
|
52881
|
+
exports.evaluateRedemptionEligibility = evaluateRedemptionEligibility;
|
|
50976
52882
|
exports.extractData = extractData;
|
|
50977
52883
|
exports.extractDeadlineFromSigningData = extractDeadlineFromSigningData;
|
|
50978
52884
|
exports.extractIpfsCid = extractIpfsCid;
|
|
50979
52885
|
exports.extractPagination = extractPagination;
|
|
50980
52886
|
exports.fetchAllPages = fetchAllPages;
|
|
50981
52887
|
exports.getAllBuiltInFieldDefinitions = getAllBuiltInFieldDefinitions;
|
|
52888
|
+
exports.getAvailabilityStatus = getAvailabilityStatus;
|
|
50982
52889
|
exports.getBuiltInFieldDefinition = getBuiltInFieldDefinition;
|
|
50983
52890
|
exports.getMetadataFromTokenUnitResponse = getMetadataFromTokenUnitResponse;
|
|
50984
52891
|
exports.getOperatorText = getOperatorText;
|
|
@@ -50987,6 +52894,7 @@ exports.getSelectOptionsLabelDefault = getSelectOptionsLabelDefault;
|
|
|
50987
52894
|
exports.getSigningUrl = getSigningUrl;
|
|
50988
52895
|
exports.globalCacheService = globalCacheService;
|
|
50989
52896
|
exports.hasMinimumRole = hasMinimumRole;
|
|
52897
|
+
exports.inheritTranslations = inheritTranslations;
|
|
50990
52898
|
exports.initializeReactNativePolyfills = initializeReactNativePolyfills;
|
|
50991
52899
|
exports.isBookingStatusValid = isBookingStatusValid;
|
|
50992
52900
|
exports.isBuiltInUserField = isBuiltInUserField;
|
|
@@ -50996,6 +52904,8 @@ exports.isErrorMessage = isErrorMessage;
|
|
|
50996
52904
|
exports.isEventMessage = isEventMessage;
|
|
50997
52905
|
exports.isFatalAuthErrorInMessage = isFatalAuthErrorInMessage;
|
|
50998
52906
|
exports.isIpfsUrl = isIpfsUrl;
|
|
52907
|
+
exports.isNftTokenType = isNftTokenType;
|
|
52908
|
+
exports.isNotificationPendingMessage = isNotificationPendingMessage;
|
|
50999
52909
|
exports.isPaginatedResponse = isPaginatedResponse;
|
|
51000
52910
|
exports.isPingMessage = isPingMessage;
|
|
51001
52911
|
exports.isServerMessage = isServerMessage;
|
|
@@ -51005,6 +52915,7 @@ exports.isUUID = isUUID;
|
|
|
51005
52915
|
exports.isUnsubscribedMessage = isUnsubscribedMessage;
|
|
51006
52916
|
exports.isUserIdentifierObject = isUserIdentifierObject;
|
|
51007
52917
|
exports.isValidBusinessMembershipRelation = isValidBusinessMembershipRelation;
|
|
52918
|
+
exports.isValidBusinessRelation = isValidBusinessRelation;
|
|
51008
52919
|
exports.isValidCampaignClaimRelation = isValidCampaignClaimRelation;
|
|
51009
52920
|
exports.isValidCampaignRelation = isValidCampaignRelation;
|
|
51010
52921
|
exports.isValidEmail = isValidEmail;
|
|
@@ -51019,16 +52930,23 @@ exports.isValidUrl = isValidUrl;
|
|
|
51019
52930
|
exports.isValidUserRelation = isValidUserRelation;
|
|
51020
52931
|
exports.isValidUuid = isValidUuid;
|
|
51021
52932
|
exports.isValidWalletAddress = isValidWalletAddress;
|
|
52933
|
+
exports.localizeFields = localizeFields;
|
|
52934
|
+
exports.meetsMinimumStatusRequirement = meetsMinimumStatusRequirement;
|
|
52935
|
+
exports.mergeTranslations = mergeTranslations;
|
|
51022
52936
|
exports.mergeWithDefaults = mergeWithDefaults;
|
|
51023
52937
|
exports.needsExternalSigning = needsExternalSigning;
|
|
51024
52938
|
exports.needsSubmission = needsSubmission;
|
|
51025
52939
|
exports.normalizeFilters = normalizeFilters;
|
|
52940
|
+
exports.normalizeLocale = normalizeLocale;
|
|
51026
52941
|
exports.normalizeToPaginated = normalizeToPaginated;
|
|
51027
52942
|
exports.operatorText = operatorText;
|
|
51028
52943
|
exports.parseUserAgentForPlatform = parseUserAgentForPlatform;
|
|
51029
52944
|
exports.parseUtmParams = parseUtmParams;
|
|
52945
|
+
exports.pickTranslationFields = pickTranslationFields;
|
|
51030
52946
|
exports.registerIdentifierType = registerIdentifierType;
|
|
52947
|
+
exports.resolveTranslation = resolveTranslation;
|
|
51031
52948
|
exports.selectOptionsSourceEntities = selectOptionsSourceEntities;
|
|
52949
|
+
exports.setIncludeParam = setIncludeParam;
|
|
51032
52950
|
exports.testnetPrefix = testnetPrefix;
|
|
51033
52951
|
exports.testnetShortPrefix = testnetShortPrefix;
|
|
51034
52952
|
exports.uniqueUserIdentifierKeys = uniqueUserIdentifierKeys;
|
|
@@ -51060,5 +52978,7 @@ exports.userModelFields = userModelFields;
|
|
|
51060
52978
|
exports.validateAllFields = validateAllFields;
|
|
51061
52979
|
exports.validateFieldComparisons = validateFieldComparisons;
|
|
51062
52980
|
exports.validateFieldValue = validateFieldValue;
|
|
52981
|
+
exports.validateTranslationsLimits = validateTranslationsLimits;
|
|
52982
|
+
exports.validateTranslationsPatch = validateTranslationsPatch;
|
|
51063
52983
|
exports.warnIfProblematicEnvironment = warnIfProblematicEnvironment;
|
|
51064
52984
|
//# sourceMappingURL=index.js.map
|