@explorins/pers-sdk-react-native 2.2.0-beta.1 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/__tests__/PersSDKProvider.test.d.ts +2 -0
- package/dist/__tests__/PersSDKProvider.test.d.ts.map +1 -0
- package/dist/__tests__/PersSDKProvider.test.js +290 -0
- package/dist/__tests__/setup.d.ts +2 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +35 -0
- package/dist/hooks/useRedemptions.d.ts +1 -0
- package/dist/hooks/useRedemptions.d.ts.map +1 -1
- package/dist/hooks/useRedemptions.js +69 -3
- package/dist/hooks/useTokenBalances.d.ts +34 -1
- package/dist/hooks/useTokenBalances.d.ts.map +1 -1
- package/dist/hooks/useTokens.d.ts +4 -1
- package/dist/hooks/useTokens.d.ts.map +1 -1
- package/dist/hooks/useTokens.js +96 -0
- package/dist/index.js +948 -81
- package/dist/index.js.map +1 -1
- package/dist/providers/PersSDKProvider.d.ts +36 -2
- package/dist/providers/PersSDKProvider.d.ts.map +1 -1
- package/dist/providers/PersSDKProvider.js +1 -1
- package/package.json +12 -2
- package/src/__tests__/PersSDKProvider.test.tsx +420 -0
- package/src/__tests__/setup.ts +40 -0
- package/src/hooks/useRedemptions.ts +74 -4
- package/src/hooks/useTokenBalances.ts +34 -1
- package/src/hooks/useTokens.ts +100 -1
- package/src/providers/PersSDKProvider.tsx +37 -3
package/dist/index.js
CHANGED
|
@@ -3216,6 +3216,12 @@ const EntityTypes = {
|
|
|
3216
3216
|
SIGNING_ACCOUNT: 'signing_account',
|
|
3217
3217
|
API_KEY: 'api_key',
|
|
3218
3218
|
WEBHOOK: 'webhook',
|
|
3219
|
+
// Service domains
|
|
3220
|
+
ANALYTICS: 'analytics',
|
|
3221
|
+
CHAIN: 'chain',
|
|
3222
|
+
FILE: 'file',
|
|
3223
|
+
JWKS: 'jwks',
|
|
3224
|
+
PROJECT: 'project',
|
|
3219
3225
|
};
|
|
3220
3226
|
/**
|
|
3221
3227
|
* Error-specific domains (extends EntityTypes)
|
|
@@ -3726,6 +3732,9 @@ const CommonErrorCodes = {
|
|
|
3726
3732
|
TOKEN_NOT_ACTIVE: 'TOKEN_NOT_ACTIVE',
|
|
3727
3733
|
TOKEN_ID_REQUIRED: 'TOKEN_ID_REQUIRED',
|
|
3728
3734
|
TOKEN_TYPE_NOT_FOUND: 'TOKEN_TYPE_NOT_FOUND',
|
|
3735
|
+
TOKEN_UNIT_NOT_FOUND: 'TOKEN_UNIT_NOT_FOUND',
|
|
3736
|
+
NFT_EXPIRED: 'NFT_EXPIRED',
|
|
3737
|
+
NFT_BUSINESS_RESTRICTION: 'NFT_BUSINESS_RESTRICTION',
|
|
3729
3738
|
// Wallet domain
|
|
3730
3739
|
WALLET_MISSING_SIGNING: 'WALLET_MISSING_SIGNING',
|
|
3731
3740
|
WALLET_NOT_FOUND: 'WALLET_NOT_FOUND',
|
|
@@ -3765,6 +3774,9 @@ const CommonErrorCodes = {
|
|
|
3765
3774
|
CAMPAIGN_DAILY_LIMIT_REACHED: 'CAMPAIGN_DAILY_LIMIT_REACHED',
|
|
3766
3775
|
CAMPAIGN_GLOBAL_LIMIT_REACHED: 'CAMPAIGN_GLOBAL_LIMIT_REACHED',
|
|
3767
3776
|
CAMPAIGN_GLOBAL_DAILY_LIMIT_REACHED: 'CAMPAIGN_GLOBAL_DAILY_LIMIT_REACHED',
|
|
3777
|
+
CAMPAIGN_NOT_FOUND: 'CAMPAIGN_NOT_FOUND',
|
|
3778
|
+
GEOGRAPHIC_RESTRICTION: 'GEOGRAPHIC_RESTRICTION',
|
|
3779
|
+
LOCATION_VERIFICATION_REQUIRED: 'LOCATION_VERIFICATION_REQUIRED',
|
|
3768
3780
|
// Trigger domain
|
|
3769
3781
|
TRIGGER_NOT_FOUND: 'TRIGGER_NOT_FOUND',
|
|
3770
3782
|
// Trigger Source domain
|
|
@@ -3780,6 +3792,12 @@ const CommonErrorCodes = {
|
|
|
3780
3792
|
EXTERNAL_SERVICE_ERROR: 'EXTERNAL_SERVICE_ERROR',
|
|
3781
3793
|
AUTHENTICATION_REQUIRED: 'AUTHENTICATION_REQUIRED',
|
|
3782
3794
|
AUTHORIZATION_FAILED: 'AUTHORIZATION_FAILED',
|
|
3795
|
+
CONFIGURATION_MISSING: 'CONFIGURATION_MISSING',
|
|
3796
|
+
// Redemption domain
|
|
3797
|
+
USER_STATUS_RESTRICTED: 'USER_STATUS_RESTRICTED',
|
|
3798
|
+
REDEMPTION_ALREADY_PROCESSING: 'REDEMPTION_ALREADY_PROCESSING',
|
|
3799
|
+
MAX_REDEMPTION: 'MAX_REDEMPTION',
|
|
3800
|
+
USER_LIMIT: 'USER_LIMIT',
|
|
3783
3801
|
// Authentication errors
|
|
3784
3802
|
INVALID_TOKEN: 'INVALID_TOKEN',
|
|
3785
3803
|
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
|
|
@@ -3787,6 +3805,8 @@ const CommonErrorCodes = {
|
|
|
3787
3805
|
REFRESH_TOKEN_EXPIRED: 'REFRESH_TOKEN_EXPIRED',
|
|
3788
3806
|
REFRESH_TOKEN_REVOKED: 'REFRESH_TOKEN_REVOKED',
|
|
3789
3807
|
FIREBASE_AUTH_ERROR: 'FIREBASE_AUTH_ERROR',
|
|
3808
|
+
MULTIPLE_CONTEXT_SELECTION_REQUIRED: 'MULTIPLE_CONTEXT_SELECTION_REQUIRED',
|
|
3809
|
+
ADMIN_TENANT_REQUIRED: 'ADMIN_TENANT_REQUIRED',
|
|
3790
3810
|
// Generic fallbacks
|
|
3791
3811
|
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
3792
3812
|
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
@@ -3812,26 +3832,49 @@ const CommonErrorCodes = {
|
|
|
3812
3832
|
* Native implementation without external dependencies for optimal performance
|
|
3813
3833
|
*/
|
|
3814
3834
|
/**
|
|
3815
|
-
*
|
|
3835
|
+
* Decode JWT payload without verification
|
|
3816
3836
|
* @param token JWT token (with or without Bearer prefix)
|
|
3817
|
-
* @
|
|
3837
|
+
* @returns Decoded payload or null if invalid
|
|
3818
3838
|
*/
|
|
3819
|
-
const
|
|
3839
|
+
const decodeJwtPayload = (token) => {
|
|
3820
3840
|
const cleanToken = token.replace(/^Bearer\s+/i, '');
|
|
3821
3841
|
if (!cleanToken || cleanToken.split('.').length !== 3) {
|
|
3822
|
-
return
|
|
3842
|
+
return null;
|
|
3823
3843
|
}
|
|
3824
3844
|
try {
|
|
3825
|
-
|
|
3826
|
-
if (!payload.exp)
|
|
3827
|
-
return false;
|
|
3828
|
-
const currentTime = Math.floor(Date.now() / 1000);
|
|
3829
|
-
return payload.exp < (currentTime + margin);
|
|
3845
|
+
return JSON.parse(atob(cleanToken.split('.')[1]));
|
|
3830
3846
|
}
|
|
3831
3847
|
catch {
|
|
3832
|
-
return
|
|
3848
|
+
return null;
|
|
3833
3849
|
}
|
|
3834
3850
|
};
|
|
3851
|
+
/**
|
|
3852
|
+
* Check if JWT token is expired or expiring within margin
|
|
3853
|
+
* @param token JWT token (with or without Bearer prefix)
|
|
3854
|
+
* @param margin Safety margin in seconds before expiry (default: 60)
|
|
3855
|
+
*/
|
|
3856
|
+
const isTokenExpired = (token, margin = 60) => {
|
|
3857
|
+
const payload = decodeJwtPayload(token);
|
|
3858
|
+
if (!payload)
|
|
3859
|
+
return true;
|
|
3860
|
+
if (!payload.exp)
|
|
3861
|
+
return false;
|
|
3862
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
3863
|
+
return payload.exp < (currentTime + margin);
|
|
3864
|
+
};
|
|
3865
|
+
/**
|
|
3866
|
+
* Get remaining time-to-live for a JWT token in seconds
|
|
3867
|
+
* @param token JWT token (with or without Bearer prefix)
|
|
3868
|
+
* @returns Remaining TTL in seconds, or null if invalid/expired
|
|
3869
|
+
*/
|
|
3870
|
+
const getTokenTimeToLive = (token) => {
|
|
3871
|
+
const payload = decodeJwtPayload(token);
|
|
3872
|
+
if (!payload?.exp)
|
|
3873
|
+
return null;
|
|
3874
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
3875
|
+
const ttl = payload.exp - currentTime;
|
|
3876
|
+
return ttl > 0 ? ttl : null;
|
|
3877
|
+
};
|
|
3835
3878
|
|
|
3836
3879
|
/**
|
|
3837
3880
|
* PERS SDK Error Handling - Optimized for Performance
|
|
@@ -3860,12 +3903,12 @@ class ErrorUtils {
|
|
|
3860
3903
|
return false;
|
|
3861
3904
|
const err = error;
|
|
3862
3905
|
// Check for explicit token expired indicators
|
|
3863
|
-
if (err?.code ===
|
|
3906
|
+
if (err?.code === CommonErrorCodes.TOKEN_EXPIRED || err?.errorCode === CommonErrorCodes.TOKEN_EXPIRED) {
|
|
3864
3907
|
return true;
|
|
3865
3908
|
}
|
|
3866
3909
|
// Check in response data
|
|
3867
3910
|
const apiError = err?.response?.data || err;
|
|
3868
|
-
if (apiError?.code ===
|
|
3911
|
+
if (apiError?.code === CommonErrorCodes.TOKEN_EXPIRED || apiError?.errorCode === CommonErrorCodes.TOKEN_EXPIRED) {
|
|
3869
3912
|
return true;
|
|
3870
3913
|
}
|
|
3871
3914
|
// Any 401 error is treated as token expiration
|
|
@@ -3935,7 +3978,9 @@ class ErrorUtils {
|
|
|
3935
3978
|
// Try extraction strategies in priority order
|
|
3936
3979
|
const candidates = [
|
|
3937
3980
|
this.parseHttpClientError(error), // React Native format
|
|
3938
|
-
httpError.response
|
|
3981
|
+
httpError.response && typeof httpError.response === 'object'
|
|
3982
|
+
? httpError.response.data
|
|
3983
|
+
: undefined, // Axios/fetch format
|
|
3939
3984
|
httpError.error, // Angular HttpErrorResponse
|
|
3940
3985
|
httpError // Direct error object
|
|
3941
3986
|
];
|
|
@@ -3954,8 +3999,9 @@ class ErrorUtils {
|
|
|
3954
3999
|
* Check if object has structured error fields from backend (StructuredError)
|
|
3955
4000
|
*/
|
|
3956
4001
|
static hasStructuredErrorFields(obj) {
|
|
3957
|
-
return obj && typeof obj === 'object' &&
|
|
3958
|
-
((
|
|
4002
|
+
return obj !== null && typeof obj === 'object' &&
|
|
4003
|
+
(('code' in obj && 'category' in obj) ||
|
|
4004
|
+
('status' in obj && ('detail' in obj || 'message' in obj || 'title' in obj)));
|
|
3959
4005
|
}
|
|
3960
4006
|
/**
|
|
3961
4007
|
* Create fallback error structure when no structured error found
|
|
@@ -3966,9 +4012,9 @@ class ErrorUtils {
|
|
|
3966
4012
|
title: 'Request failed',
|
|
3967
4013
|
message: this.getMessage(error),
|
|
3968
4014
|
detail: this.getMessage(error),
|
|
3969
|
-
code:
|
|
4015
|
+
code: CommonErrorCodes.TECHNICAL_ERROR,
|
|
3970
4016
|
category: exports.ErrorCategory.TECHNICAL,
|
|
3971
|
-
domain:
|
|
4017
|
+
domain: ErrorDomains.EXTERNAL,
|
|
3972
4018
|
retryable: this.isRetryable(error)
|
|
3973
4019
|
};
|
|
3974
4020
|
}
|
|
@@ -4047,7 +4093,7 @@ class PersApiError extends Error {
|
|
|
4047
4093
|
this.method = method;
|
|
4048
4094
|
this.name = 'PersApiError';
|
|
4049
4095
|
// Use backend error details directly
|
|
4050
|
-
this.code = backendError?.code ||
|
|
4096
|
+
this.code = backendError?.code || CommonErrorCodes.TECHNICAL_ERROR;
|
|
4051
4097
|
this.domain = backendError?.domain || ErrorDomains.EXTERNAL;
|
|
4052
4098
|
this.category = backendError?.category || exports.ErrorCategory.TECHNICAL;
|
|
4053
4099
|
this.retryable = backendError?.retryable ?? (statusCode ? statusCode >= 500 : false);
|
|
@@ -4071,7 +4117,7 @@ class AuthenticationError extends PersApiError {
|
|
|
4071
4117
|
class NetworkError extends Error {
|
|
4072
4118
|
constructor(message) {
|
|
4073
4119
|
super(message);
|
|
4074
|
-
this.code =
|
|
4120
|
+
this.code = CommonErrorCodes.EXTERNAL_SERVICE_ERROR;
|
|
4075
4121
|
this.domain = ErrorDomains.EXTERNAL;
|
|
4076
4122
|
this.category = exports.ErrorCategory.INFRASTRUCTURE;
|
|
4077
4123
|
this.retryable = true;
|
|
@@ -5119,6 +5165,58 @@ class TokenApi {
|
|
|
5119
5165
|
}
|
|
5120
5166
|
return this.apiClient.get(url);
|
|
5121
5167
|
}
|
|
5168
|
+
/**
|
|
5169
|
+
* Get all token metadata with filtering and pagination
|
|
5170
|
+
*
|
|
5171
|
+
* Useful for displaying rewards (ERC1155) or stamps (ERC721) in tables.
|
|
5172
|
+
* Non-admin users always get active metadata only.
|
|
5173
|
+
*
|
|
5174
|
+
* @param options - Filter and pagination options
|
|
5175
|
+
* @returns Paginated response with token metadata
|
|
5176
|
+
*
|
|
5177
|
+
* @example Get all active rewards
|
|
5178
|
+
* ```typescript
|
|
5179
|
+
* const rewards = await tokenApi.getTokenMetadata({
|
|
5180
|
+
* tokenType: 'ERC1155',
|
|
5181
|
+
* active: true,
|
|
5182
|
+
* limit: 10
|
|
5183
|
+
* });
|
|
5184
|
+
* ```
|
|
5185
|
+
*
|
|
5186
|
+
* @example Get stamps with search
|
|
5187
|
+
* ```typescript
|
|
5188
|
+
* const stamps = await tokenApi.getTokenMetadata({
|
|
5189
|
+
* tokenType: 'ERC721',
|
|
5190
|
+
* search: 'gold',
|
|
5191
|
+
* sortBy: 'name',
|
|
5192
|
+
* sortOrder: 'ASC'
|
|
5193
|
+
* });
|
|
5194
|
+
* ```
|
|
5195
|
+
*/
|
|
5196
|
+
async getTokenMetadata(options) {
|
|
5197
|
+
const params = buildPaginationParams(options);
|
|
5198
|
+
if (options?.active !== undefined) {
|
|
5199
|
+
params.set('active', String(options.active));
|
|
5200
|
+
}
|
|
5201
|
+
if (options?.tokenType) {
|
|
5202
|
+
params.set('tokenType', options.tokenType);
|
|
5203
|
+
}
|
|
5204
|
+
if (options?.tokenId) {
|
|
5205
|
+
params.set('tokenId', options.tokenId);
|
|
5206
|
+
}
|
|
5207
|
+
if (options?.search) {
|
|
5208
|
+
params.set('search', options.search);
|
|
5209
|
+
}
|
|
5210
|
+
// Handle tags as string or array
|
|
5211
|
+
if (options?.tags) {
|
|
5212
|
+
const tagsArray = Array.isArray(options.tags) ? options.tags : [options.tags];
|
|
5213
|
+
if (tagsArray.length > 0) {
|
|
5214
|
+
params.set('tags', tagsArray.join(','));
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
const response = await this.apiClient.get(`${this.basePath}/metadata?${params.toString()}`);
|
|
5218
|
+
return normalizeToPaginated(response);
|
|
5219
|
+
}
|
|
5122
5220
|
// ==========================================
|
|
5123
5221
|
// ADMIN OPERATIONS
|
|
5124
5222
|
// ==========================================
|
|
@@ -5228,6 +5326,40 @@ class TokenService {
|
|
|
5228
5326
|
async getTokenByContractAddress(contractAddress, contractTokenId) {
|
|
5229
5327
|
return this.tokenApi.getTokenByContractAddress(contractAddress, contractTokenId);
|
|
5230
5328
|
}
|
|
5329
|
+
/**
|
|
5330
|
+
* Get all token metadata with filtering and pagination
|
|
5331
|
+
*
|
|
5332
|
+
* Useful for displaying rewards (ERC1155) or stamps (ERC721) in tables.
|
|
5333
|
+
* Non-admin users always get active metadata only.
|
|
5334
|
+
*
|
|
5335
|
+
* @param options - Filter and pagination options
|
|
5336
|
+
* @returns Paginated response with token metadata
|
|
5337
|
+
*/
|
|
5338
|
+
async getTokenMetadata(options) {
|
|
5339
|
+
return this.tokenApi.getTokenMetadata(options);
|
|
5340
|
+
}
|
|
5341
|
+
/**
|
|
5342
|
+
* Get rewards (ERC1155 token metadata) with filtering and pagination
|
|
5343
|
+
*
|
|
5344
|
+
* Convenience method for fetching reward metadata.
|
|
5345
|
+
*
|
|
5346
|
+
* @param options - Filter and pagination options (tokenType is set automatically)
|
|
5347
|
+
* @returns Paginated response with reward metadata
|
|
5348
|
+
*/
|
|
5349
|
+
async getRewards(options) {
|
|
5350
|
+
return this.tokenApi.getTokenMetadata({ ...options, tokenType: NativeTokenTypes.ERC1155 });
|
|
5351
|
+
}
|
|
5352
|
+
/**
|
|
5353
|
+
* Get stamps (ERC721 token metadata) with filtering and pagination
|
|
5354
|
+
*
|
|
5355
|
+
* Convenience method for fetching stamp metadata.
|
|
5356
|
+
*
|
|
5357
|
+
* @param options - Filter and pagination options (tokenType is set automatically)
|
|
5358
|
+
* @returns Paginated response with stamp metadata
|
|
5359
|
+
*/
|
|
5360
|
+
async getStamps(options) {
|
|
5361
|
+
return this.tokenApi.getTokenMetadata({ ...options, tokenType: NativeTokenTypes.ERC721 });
|
|
5362
|
+
}
|
|
5231
5363
|
// ==========================================
|
|
5232
5364
|
// ADMIN OPERATIONS
|
|
5233
5365
|
// ==========================================
|
|
@@ -5354,7 +5486,7 @@ class BusinessApi {
|
|
|
5354
5486
|
/**
|
|
5355
5487
|
* Get all businesses with role-based filtering and pagination
|
|
5356
5488
|
*
|
|
5357
|
-
* Endpoint: GET /businesses?active={boolean}&sanitize={mode}&page={number}&limit={number}
|
|
5489
|
+
* Endpoint: GET /businesses?active={boolean}&sanitize={mode}&page={number}&limit={number}&businessTypeId={number}
|
|
5358
5490
|
* Auth: @ApiSecurity('projectKey') (enhanced with role-based filtering)
|
|
5359
5491
|
*
|
|
5360
5492
|
* Note:
|
|
@@ -5377,8 +5509,11 @@ class BusinessApi {
|
|
|
5377
5509
|
* });
|
|
5378
5510
|
*
|
|
5379
5511
|
* // Filter by tag(s)
|
|
5380
|
-
* const tagged = await businessApi.getBusinesses({
|
|
5381
|
-
* const multiTag = await businessApi.getBusinesses({
|
|
5512
|
+
* const tagged = await businessApi.getBusinesses({ tags: 'restaurant' });
|
|
5513
|
+
* const multiTag = await businessApi.getBusinesses({ tags: ['restaurant', 'cafe'] });
|
|
5514
|
+
*
|
|
5515
|
+
* // Filter by business type
|
|
5516
|
+
* const hotels = await businessApi.getBusinesses({ businessTypeId: 1 });
|
|
5382
5517
|
* ```
|
|
5383
5518
|
*/
|
|
5384
5519
|
async getBusinesses(options) {
|
|
@@ -5398,6 +5533,9 @@ class BusinessApi {
|
|
|
5398
5533
|
if (options?.sanitize) {
|
|
5399
5534
|
params.set('sanitize', options.sanitize);
|
|
5400
5535
|
}
|
|
5536
|
+
if (options?.businessTypeId !== undefined) {
|
|
5537
|
+
params.set('businessTypeId', String(options.businessTypeId));
|
|
5538
|
+
}
|
|
5401
5539
|
const response = await this.apiClient.get(`${this.basePath}?${params.toString()}`);
|
|
5402
5540
|
return normalizeToPaginated(response);
|
|
5403
5541
|
}
|
|
@@ -5744,20 +5882,16 @@ class BusinessService {
|
|
|
5744
5882
|
return this.businessApi.getBusinessByAccount(accountAddress);
|
|
5745
5883
|
}
|
|
5746
5884
|
/**
|
|
5747
|
-
* Get businesses by type (
|
|
5748
|
-
*
|
|
5885
|
+
* Get businesses by type (server-side filtering)
|
|
5886
|
+
* Uses the businessTypeId parameter for efficient server-side filtering.
|
|
5749
5887
|
*/
|
|
5750
5888
|
async getBusinessesByType(typeId, options) {
|
|
5751
|
-
const
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
total: filtered.length,
|
|
5758
|
-
pages: Math.ceil(filtered.length / (response.pagination.limit || 20))
|
|
5759
|
-
}
|
|
5760
|
-
};
|
|
5889
|
+
const businessTypeId = parseInt(typeId, 10);
|
|
5890
|
+
return this.businessApi.getBusinesses({
|
|
5891
|
+
...options,
|
|
5892
|
+
businessTypeId,
|
|
5893
|
+
active: options?.active ?? true // Default to active businesses
|
|
5894
|
+
});
|
|
5761
5895
|
}
|
|
5762
5896
|
// ==========================================
|
|
5763
5897
|
// ADMIN OPERATIONS
|
|
@@ -6996,6 +7130,14 @@ class RedemptionService {
|
|
|
6996
7130
|
async getRedemptions(options) {
|
|
6997
7131
|
return this.redemptionApi.getRedemptions(options);
|
|
6998
7132
|
}
|
|
7133
|
+
/**
|
|
7134
|
+
* Get a specific redemption by ID
|
|
7135
|
+
* @param id - Redemption UUID
|
|
7136
|
+
* @param include - Optional relations to include: 'redeemCount'
|
|
7137
|
+
*/
|
|
7138
|
+
async getRedemptionById(id, include) {
|
|
7139
|
+
return this.redemptionApi.getRedemptionById(id, include);
|
|
7140
|
+
}
|
|
6999
7141
|
/**
|
|
7000
7142
|
* Convenience: Get active redemptions (public)
|
|
7001
7143
|
*/
|
|
@@ -8635,6 +8777,49 @@ class AnalyticsApi {
|
|
|
8635
8777
|
async getCampaignClaimAnalytics(request) {
|
|
8636
8778
|
return this.apiClient.post('/analytics/campaign-claims', request);
|
|
8637
8779
|
}
|
|
8780
|
+
/**
|
|
8781
|
+
* ADMIN: Get redemption redeem analytics with aggregation (charts, metrics, grouping)
|
|
8782
|
+
*
|
|
8783
|
+
* This endpoint is for aggregated analytics only (groupBy/metrics).
|
|
8784
|
+
* For enriched list data with nested objects, use redemption.getRedeems() instead.
|
|
8785
|
+
*
|
|
8786
|
+
* @example Redeems per redemption
|
|
8787
|
+
* ```typescript
|
|
8788
|
+
* const response = await analyticsApi.getRedemptionRedeemAnalytics({
|
|
8789
|
+
* groupBy: ['redemptionId'],
|
|
8790
|
+
* metrics: ['count'],
|
|
8791
|
+
* sortBy: 'count',
|
|
8792
|
+
* sortOrder: SortOrder.DESC,
|
|
8793
|
+
* limit: 10
|
|
8794
|
+
* });
|
|
8795
|
+
* ```
|
|
8796
|
+
*
|
|
8797
|
+
* @example Redeems by status over time
|
|
8798
|
+
* ```typescript
|
|
8799
|
+
* const response = await analyticsApi.getRedemptionRedeemAnalytics({
|
|
8800
|
+
* groupBy: ['month', 'status'],
|
|
8801
|
+
* metrics: ['count'],
|
|
8802
|
+
* sortBy: 'month',
|
|
8803
|
+
* sortOrder: SortOrder.DESC,
|
|
8804
|
+
* startDate: new Date('2026-01-01'),
|
|
8805
|
+
* endDate: new Date('2026-12-31')
|
|
8806
|
+
* });
|
|
8807
|
+
* ```
|
|
8808
|
+
*
|
|
8809
|
+
* @example Redeems by business
|
|
8810
|
+
* ```typescript
|
|
8811
|
+
* const response = await analyticsApi.getRedemptionRedeemAnalytics({
|
|
8812
|
+
* filters: { status: 'COMPLETED' },
|
|
8813
|
+
* groupBy: ['businessId'],
|
|
8814
|
+
* metrics: ['count'],
|
|
8815
|
+
* sortBy: 'count',
|
|
8816
|
+
* sortOrder: SortOrder.DESC
|
|
8817
|
+
* });
|
|
8818
|
+
* ```
|
|
8819
|
+
*/
|
|
8820
|
+
async getRedemptionRedeemAnalytics(request) {
|
|
8821
|
+
return this.apiClient.post('/analytics/redemption-redeems', request);
|
|
8822
|
+
}
|
|
8638
8823
|
/**
|
|
8639
8824
|
* ADMIN: Get user analytics with engagement metrics
|
|
8640
8825
|
*
|
|
@@ -8866,6 +9051,15 @@ class AnalyticsService {
|
|
|
8866
9051
|
async getCampaignClaimAnalytics(request) {
|
|
8867
9052
|
return this.analyticsApi.getCampaignClaimAnalytics(request);
|
|
8868
9053
|
}
|
|
9054
|
+
/**
|
|
9055
|
+
* ADMIN: Get redemption redeem analytics with aggregation
|
|
9056
|
+
*
|
|
9057
|
+
* Returns aggregated redeem data with flexible grouping and metrics.
|
|
9058
|
+
* Perfect for charts and dashboard summaries.
|
|
9059
|
+
*/
|
|
9060
|
+
async getRedemptionRedeemAnalytics(request) {
|
|
9061
|
+
return this.analyticsApi.getRedemptionRedeemAnalytics(request);
|
|
9062
|
+
}
|
|
8869
9063
|
/**
|
|
8870
9064
|
* ADMIN: Get user analytics with engagement metrics
|
|
8871
9065
|
*
|
|
@@ -9323,6 +9517,8 @@ function isExtendedProvider(provider) {
|
|
|
9323
9517
|
* TOKEN_EXPIRED is NOT in this list - it's the normal "please refresh" case.
|
|
9324
9518
|
*
|
|
9325
9519
|
* Values validated against CommonErrorCodes from @explorins/pers-shared at compile time.
|
|
9520
|
+
*
|
|
9521
|
+
* SINGLE SOURCE OF TRUTH - use this constant everywhere, never duplicate these values.
|
|
9326
9522
|
*/
|
|
9327
9523
|
const FATAL_AUTH_CODES = [
|
|
9328
9524
|
'REFRESH_TOKEN_EXPIRED',
|
|
@@ -9330,6 +9526,13 @@ const FATAL_AUTH_CODES = [
|
|
|
9330
9526
|
'INVALID_TOKEN',
|
|
9331
9527
|
'TOKEN_REVOKED'
|
|
9332
9528
|
];
|
|
9529
|
+
/**
|
|
9530
|
+
* Check if an error message contains any fatal auth error code.
|
|
9531
|
+
* Useful for detecting fatal errors in wrapped/stringified errors.
|
|
9532
|
+
*/
|
|
9533
|
+
function isFatalAuthErrorInMessage(message) {
|
|
9534
|
+
return FATAL_AUTH_CODES.some(code => message.includes(code));
|
|
9535
|
+
}
|
|
9333
9536
|
/**
|
|
9334
9537
|
* Platform-agnostic authentication service
|
|
9335
9538
|
* Handles login, token refresh, and storage operations
|
|
@@ -9485,8 +9688,58 @@ class AuthService {
|
|
|
9485
9688
|
return FATAL_AUTH_CODES.includes(errorCode);
|
|
9486
9689
|
}
|
|
9487
9690
|
/**
|
|
9488
|
-
*
|
|
9489
|
-
*
|
|
9691
|
+
* Comprehensive check if an error represents a DEFINITIVE auth failure.
|
|
9692
|
+
* This is the SINGLE SOURCE OF TRUTH for determining if logout is required.
|
|
9693
|
+
*
|
|
9694
|
+
* Returns true for:
|
|
9695
|
+
* - Known fatal error codes (REFRESH_TOKEN_EXPIRED, TOKEN_REVOKED, etc.)
|
|
9696
|
+
* - Fatal error codes in error messages (wrapped errors)
|
|
9697
|
+
*
|
|
9698
|
+
* Returns false for (retryable/recoverable):
|
|
9699
|
+
* - Network errors
|
|
9700
|
+
* - Timeouts
|
|
9701
|
+
* - 5xx server errors
|
|
9702
|
+
* - TOKEN_EXPIRED (normal refresh trigger)
|
|
9703
|
+
* - Raw 401/403 without fatal code (should try refresh first)
|
|
9704
|
+
*
|
|
9705
|
+
* NOTE: We intentionally DO NOT treat raw 401/403 status as fatal.
|
|
9706
|
+
* The HTTP client handles 401 by attempting token refresh first.
|
|
9707
|
+
* Only if refresh fails WITH a fatal code do we logout.
|
|
9708
|
+
*/
|
|
9709
|
+
isDefinitiveAuthFailure(error) {
|
|
9710
|
+
if (!error)
|
|
9711
|
+
return false;
|
|
9712
|
+
const errorAny = error;
|
|
9713
|
+
// Check error code property against canonical list
|
|
9714
|
+
if (errorAny?.code && this.isFatalAuthError(errorAny.code)) {
|
|
9715
|
+
return true;
|
|
9716
|
+
}
|
|
9717
|
+
// Check if error message contains fatal keywords (fallback for wrapped errors)
|
|
9718
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
9719
|
+
if (isFatalAuthErrorInMessage(errorMessage)) {
|
|
9720
|
+
return true;
|
|
9721
|
+
}
|
|
9722
|
+
// NOTE: We intentionally DO NOT check HTTP status codes here.
|
|
9723
|
+
// A raw 401/403 without a fatal error code should trigger a refresh attempt,
|
|
9724
|
+
// not an immediate logout. The HTTP client (pers-api-client.ts) handles this:
|
|
9725
|
+
// 1. 401 received → try refresh token
|
|
9726
|
+
// 2. Refresh succeeds → retry original request
|
|
9727
|
+
// 3. Refresh fails with fatal CODE → then logout
|
|
9728
|
+
// Everything else is considered retryable
|
|
9729
|
+
return false;
|
|
9730
|
+
}
|
|
9731
|
+
/**
|
|
9732
|
+
* Handle authentication failure with provider token recovery.
|
|
9733
|
+
*
|
|
9734
|
+
* Before logging out, attempts to recover the session using the stored
|
|
9735
|
+
* provider token (Firebase JWT, etc.) if it's still valid.
|
|
9736
|
+
*
|
|
9737
|
+
* Recovery Flow:
|
|
9738
|
+
* 1. Check if provider token exists and is not expired
|
|
9739
|
+
* 2. Get the auth type (USER, BUSINESS, TENANT) to know which login to use
|
|
9740
|
+
* 3. Attempt re-authentication with provider token
|
|
9741
|
+
* 4. If successful: Session recovered, stay logged in
|
|
9742
|
+
* 5. If failed: Clear tokens and emit AUTH_FAILED
|
|
9490
9743
|
*/
|
|
9491
9744
|
async handleAuthFailure() {
|
|
9492
9745
|
// Already failed - nothing to do
|
|
@@ -9502,7 +9755,7 @@ class AuthService {
|
|
|
9502
9755
|
return await this.activeAuthFailurePromise;
|
|
9503
9756
|
}
|
|
9504
9757
|
// Create the failure promise to deduplicate concurrent calls
|
|
9505
|
-
this.activeAuthFailurePromise = this.
|
|
9758
|
+
this.activeAuthFailurePromise = this.performAuthFailureWithRecovery();
|
|
9506
9759
|
try {
|
|
9507
9760
|
await this.activeAuthFailurePromise;
|
|
9508
9761
|
}
|
|
@@ -9510,6 +9763,77 @@ class AuthService {
|
|
|
9510
9763
|
this.activeAuthFailurePromise = null;
|
|
9511
9764
|
}
|
|
9512
9765
|
}
|
|
9766
|
+
/**
|
|
9767
|
+
* Attempts to recover session using provider token before failing.
|
|
9768
|
+
*/
|
|
9769
|
+
async performAuthFailureWithRecovery() {
|
|
9770
|
+
// Clear active refresh operations
|
|
9771
|
+
this.activeRefreshPromise = null;
|
|
9772
|
+
// Try to recover using provider token
|
|
9773
|
+
const recovered = await this.attemptProviderTokenRecovery();
|
|
9774
|
+
if (recovered) {
|
|
9775
|
+
return; // Session recovered, don't fail
|
|
9776
|
+
}
|
|
9777
|
+
// Recovery failed - perform actual logout
|
|
9778
|
+
await this.performAuthFailure();
|
|
9779
|
+
}
|
|
9780
|
+
/**
|
|
9781
|
+
* Attempt to recover session using stored provider token.
|
|
9782
|
+
* Extracts context (tenantId/businessId) from the current refresh token to ensure
|
|
9783
|
+
* the recovered session maintains the same context.
|
|
9784
|
+
*
|
|
9785
|
+
* @returns true if session was successfully recovered
|
|
9786
|
+
*/
|
|
9787
|
+
async attemptProviderTokenRecovery() {
|
|
9788
|
+
const extended = this.extendedProvider;
|
|
9789
|
+
if (!extended?.getProviderToken || !extended?.getAuthType) {
|
|
9790
|
+
return false; // Provider doesn't support recovery
|
|
9791
|
+
}
|
|
9792
|
+
try {
|
|
9793
|
+
const providerToken = await extended.getProviderToken();
|
|
9794
|
+
if (!providerToken) {
|
|
9795
|
+
return false;
|
|
9796
|
+
}
|
|
9797
|
+
// Check if provider token is still valid (with small margin)
|
|
9798
|
+
if (isTokenExpired(providerToken, 30)) {
|
|
9799
|
+
return false;
|
|
9800
|
+
}
|
|
9801
|
+
const authType = await extended.getAuthType();
|
|
9802
|
+
if (!authType) {
|
|
9803
|
+
return false;
|
|
9804
|
+
}
|
|
9805
|
+
// Extract context (tenantId/businessId) from the current refresh token
|
|
9806
|
+
// This ensures multi-tenant/multi-business users recover to the same context
|
|
9807
|
+
const refreshToken = await this.authProvider?.getRefreshToken?.();
|
|
9808
|
+
const contextClaims = refreshToken ? decodeJwtPayload(refreshToken) : null;
|
|
9809
|
+
// Re-authenticate using provider token based on auth type
|
|
9810
|
+
// Pass the context from the previous session to avoid MULTIPLE_CONTEXT_SELECTION_REQUIRED
|
|
9811
|
+
// Note: In AuthJWTPayload, when accountType=BUSINESS, sub contains the businessId
|
|
9812
|
+
switch (authType) {
|
|
9813
|
+
case exports.AccountOwnerType.USER:
|
|
9814
|
+
await this.loginUser(providerToken);
|
|
9815
|
+
break;
|
|
9816
|
+
case exports.AccountOwnerType.BUSINESS:
|
|
9817
|
+
// For BUSINESS auth, sub contains the businessId when accountType is BUSINESS
|
|
9818
|
+
const businessId = contextClaims?.accountType === exports.AccountOwnerType.BUSINESS
|
|
9819
|
+
? contextClaims.sub
|
|
9820
|
+
: undefined;
|
|
9821
|
+
await this.loginBusiness(providerToken, businessId ? { businessId } : undefined);
|
|
9822
|
+
break;
|
|
9823
|
+
case exports.AccountOwnerType.TENANT:
|
|
9824
|
+
await this.loginTenantAdmin(providerToken, contextClaims?.tenantId ? { tenantId: contextClaims.tenantId } : undefined);
|
|
9825
|
+
break;
|
|
9826
|
+
default:
|
|
9827
|
+
console.warn(`[AuthService] Unknown auth type for recovery: ${authType}`);
|
|
9828
|
+
return false;
|
|
9829
|
+
}
|
|
9830
|
+
return true; // Recovery successful
|
|
9831
|
+
}
|
|
9832
|
+
catch (error) {
|
|
9833
|
+
console.warn('[AuthService] Provider token recovery failed:', error);
|
|
9834
|
+
return false;
|
|
9835
|
+
}
|
|
9836
|
+
}
|
|
9513
9837
|
/**
|
|
9514
9838
|
* Performs the actual auth failure cleanup
|
|
9515
9839
|
*/
|
|
@@ -9589,16 +9913,32 @@ class AuthService {
|
|
|
9589
9913
|
|
|
9590
9914
|
/**
|
|
9591
9915
|
* Token Refresh Manager
|
|
9592
|
-
*
|
|
9916
|
+
* Industry-standard token lifecycle management with:
|
|
9917
|
+
* - Dynamic refresh margin based on token lifetime
|
|
9918
|
+
* - Retry logic for transient failures
|
|
9919
|
+
* - Only logout on definitive auth failures (not network errors)
|
|
9593
9920
|
*/
|
|
9594
9921
|
class TokenRefreshManager {
|
|
9595
9922
|
constructor(authService, authProvider) {
|
|
9596
9923
|
this.authService = authService;
|
|
9597
9924
|
this.authProvider = authProvider;
|
|
9598
|
-
|
|
9925
|
+
// Industry standard: refresh when 25% of token lifetime remains
|
|
9926
|
+
this.REFRESH_THRESHOLD_PERCENT = 0.25;
|
|
9927
|
+
// Minimum margin regardless of token lifetime
|
|
9928
|
+
this.MIN_REFRESH_MARGIN_SECONDS = 30;
|
|
9929
|
+
// Maximum margin (cap for very long-lived tokens)
|
|
9930
|
+
this.MAX_REFRESH_MARGIN_SECONDS = 300; // 5 minutes
|
|
9931
|
+
// Validation cache to avoid redundant checks
|
|
9599
9932
|
this.lastValidationTime = 0;
|
|
9600
9933
|
this.validationCacheDurationMs = 30000; // 30 seconds
|
|
9934
|
+
// Retry configuration for transient failures
|
|
9935
|
+
this.MAX_RETRY_ATTEMPTS = 3;
|
|
9936
|
+
this.RETRY_DELAY_MS = 1000;
|
|
9601
9937
|
}
|
|
9938
|
+
/**
|
|
9939
|
+
* Ensures the access token is valid, refreshing if needed.
|
|
9940
|
+
* Uses dynamic margin based on token lifetime (industry standard).
|
|
9941
|
+
*/
|
|
9602
9942
|
async ensureValidToken() {
|
|
9603
9943
|
const now = Date.now();
|
|
9604
9944
|
// Skip validation if we checked recently (within cache duration)
|
|
@@ -9610,35 +9950,95 @@ class TokenRefreshManager {
|
|
|
9610
9950
|
if (!token) {
|
|
9611
9951
|
return;
|
|
9612
9952
|
}
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
|
|
9953
|
+
// Calculate dynamic refresh margin based on token lifetime
|
|
9954
|
+
const marginSeconds = this.calculateRefreshMargin(token);
|
|
9955
|
+
if (isTokenExpired(token, marginSeconds)) {
|
|
9956
|
+
const result = await this.attemptRefreshWithRetry();
|
|
9957
|
+
if (!result.success) {
|
|
9958
|
+
if (result.retryable) {
|
|
9959
|
+
// Transient error - don't logout, let next request retry
|
|
9960
|
+
console.warn('[TokenRefreshManager] Refresh failed with retryable error, will retry on next request');
|
|
9961
|
+
return;
|
|
9962
|
+
}
|
|
9963
|
+
// Definitive auth failure - logout required
|
|
9616
9964
|
await this.authService.handleAuthFailure();
|
|
9617
9965
|
}
|
|
9618
9966
|
else {
|
|
9619
|
-
//
|
|
9967
|
+
// Refresh successful
|
|
9620
9968
|
this.lastValidationTime = now;
|
|
9621
9969
|
}
|
|
9622
9970
|
}
|
|
9623
9971
|
else {
|
|
9624
|
-
// Token is valid
|
|
9972
|
+
// Token is valid
|
|
9625
9973
|
this.lastValidationTime = now;
|
|
9626
9974
|
}
|
|
9627
9975
|
}
|
|
9628
9976
|
catch (error) {
|
|
9629
|
-
|
|
9977
|
+
// Unexpected error - log but don't logout (conservative approach)
|
|
9978
|
+
console.error('[TokenRefreshManager] Unexpected error during token validation:', error);
|
|
9630
9979
|
}
|
|
9631
9980
|
}
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9981
|
+
/**
|
|
9982
|
+
* Calculate refresh margin dynamically based on token lifetime.
|
|
9983
|
+
* Industry standard: refresh when 25% of lifetime remains.
|
|
9984
|
+
*/
|
|
9985
|
+
calculateRefreshMargin(token) {
|
|
9986
|
+
const ttlSeconds = getTokenTimeToLive(token);
|
|
9987
|
+
if (ttlSeconds === null || ttlSeconds <= 0) {
|
|
9988
|
+
// Can't determine TTL, use minimum margin
|
|
9989
|
+
return this.MIN_REFRESH_MARGIN_SECONDS;
|
|
9636
9990
|
}
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
|
|
9991
|
+
// Calculate 25% of remaining lifetime
|
|
9992
|
+
const dynamicMargin = Math.floor(ttlSeconds * this.REFRESH_THRESHOLD_PERCENT);
|
|
9993
|
+
// Clamp between min and max
|
|
9994
|
+
return Math.max(this.MIN_REFRESH_MARGIN_SECONDS, Math.min(dynamicMargin, this.MAX_REFRESH_MARGIN_SECONDS));
|
|
9995
|
+
}
|
|
9996
|
+
/**
|
|
9997
|
+
* Attempt token refresh with retry logic for transient failures.
|
|
9998
|
+
* Only gives up on definitive auth errors.
|
|
9999
|
+
*/
|
|
10000
|
+
async attemptRefreshWithRetry() {
|
|
10001
|
+
let lastError;
|
|
10002
|
+
for (let attempt = 1; attempt <= this.MAX_RETRY_ATTEMPTS; attempt++) {
|
|
10003
|
+
try {
|
|
10004
|
+
await this.authService.refreshAccessToken();
|
|
10005
|
+
return { success: true, retryable: false };
|
|
10006
|
+
}
|
|
10007
|
+
catch (error) {
|
|
10008
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
10009
|
+
// Check if this is a definitive auth failure (non-retryable)
|
|
10010
|
+
if (this.isDefinitiveAuthFailure(error)) {
|
|
10011
|
+
console.warn(`[TokenRefreshManager] Definitive auth failure: ${lastError.message}`);
|
|
10012
|
+
return { success: false, retryable: false, error: lastError };
|
|
10013
|
+
}
|
|
10014
|
+
// Retryable error - wait and try again
|
|
10015
|
+
if (attempt < this.MAX_RETRY_ATTEMPTS) {
|
|
10016
|
+
await this.delay(this.RETRY_DELAY_MS * attempt); // Exponential backoff
|
|
10017
|
+
}
|
|
10018
|
+
}
|
|
9641
10019
|
}
|
|
10020
|
+
// All retries exhausted - return as retryable so we don't logout
|
|
10021
|
+
console.warn(`[TokenRefreshManager] All ${this.MAX_RETRY_ATTEMPTS} refresh attempts failed`);
|
|
10022
|
+
return { success: false, retryable: true, error: lastError };
|
|
10023
|
+
}
|
|
10024
|
+
/**
|
|
10025
|
+
* Determine if an error is a definitive auth failure that requires logout.
|
|
10026
|
+
* Network errors, timeouts, 5xx errors are NOT definitive - they're retryable.
|
|
10027
|
+
*
|
|
10028
|
+
* Delegates to AuthService.isDefinitiveAuthFailure() - the SINGLE SOURCE OF TRUTH.
|
|
10029
|
+
*/
|
|
10030
|
+
isDefinitiveAuthFailure(error) {
|
|
10031
|
+
return this.authService.isDefinitiveAuthFailure(error);
|
|
10032
|
+
}
|
|
10033
|
+
delay(ms) {
|
|
10034
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
10035
|
+
}
|
|
10036
|
+
/**
|
|
10037
|
+
* @deprecated Use ensureValidToken() instead
|
|
10038
|
+
*/
|
|
10039
|
+
async attemptInternalRefresh() {
|
|
10040
|
+
const result = await this.attemptRefreshWithRetry();
|
|
10041
|
+
return result.success;
|
|
9642
10042
|
}
|
|
9643
10043
|
}
|
|
9644
10044
|
|
|
@@ -9786,6 +10186,22 @@ class AuthTokenManager {
|
|
|
9786
10186
|
this.cache = {};
|
|
9787
10187
|
await this.storage.clear();
|
|
9788
10188
|
}
|
|
10189
|
+
/**
|
|
10190
|
+
* Clears only auth tokens (access, refresh, provider, authType), preserving DPoP keys.
|
|
10191
|
+
* Use this when DPoP keys are regenerated - we want to invalidate auth tokens
|
|
10192
|
+
* that were bound to old keys, but keep the newly generated DPoP keys.
|
|
10193
|
+
*/
|
|
10194
|
+
async clearAuthTokens() {
|
|
10195
|
+
// Clear cache
|
|
10196
|
+
this.cache = {};
|
|
10197
|
+
// Remove all auth tokens and auth type, NOT DPoP keys
|
|
10198
|
+
await Promise.all([
|
|
10199
|
+
this.storage.remove(AUTH_STORAGE_KEYS.ACCESS_TOKEN),
|
|
10200
|
+
this.storage.remove(AUTH_STORAGE_KEYS.REFRESH_TOKEN),
|
|
10201
|
+
this.storage.remove(AUTH_STORAGE_KEYS.PROVIDER_TOKEN),
|
|
10202
|
+
this.storage.remove(AUTH_STORAGE_KEYS.AUTH_TYPE),
|
|
10203
|
+
]);
|
|
10204
|
+
}
|
|
9789
10205
|
async hasAccessToken() {
|
|
9790
10206
|
// Use cached value if available to avoid storage read
|
|
9791
10207
|
if (this.cache.accessToken !== undefined) {
|
|
@@ -9816,13 +10232,12 @@ class IndexedDBTokenStorage {
|
|
|
9816
10232
|
constructor() {
|
|
9817
10233
|
this.supportsObjects = true;
|
|
9818
10234
|
this.dbPromise = null;
|
|
9819
|
-
|
|
9820
|
-
console.warn('IndexedDB is not available in this environment');
|
|
9821
|
-
}
|
|
10235
|
+
// IndexedDB availability is checked lazily on first operation
|
|
9822
10236
|
}
|
|
9823
10237
|
getDB() {
|
|
9824
|
-
if (this.dbPromise)
|
|
10238
|
+
if (this.dbPromise) {
|
|
9825
10239
|
return this.dbPromise;
|
|
10240
|
+
}
|
|
9826
10241
|
this.dbPromise = new Promise((resolve, reject) => {
|
|
9827
10242
|
if (typeof indexedDB === 'undefined') {
|
|
9828
10243
|
return reject(new Error('IndexedDB not supported'));
|
|
@@ -9850,8 +10265,12 @@ class IndexedDBTokenStorage {
|
|
|
9850
10265
|
const transaction = db.transaction(IndexedDBTokenStorage.STORE_NAME, 'readonly');
|
|
9851
10266
|
const store = transaction.objectStore(IndexedDBTokenStorage.STORE_NAME);
|
|
9852
10267
|
const request = store.get(key);
|
|
9853
|
-
request.onsuccess = () =>
|
|
9854
|
-
|
|
10268
|
+
request.onsuccess = () => {
|
|
10269
|
+
resolve(request.result || null);
|
|
10270
|
+
};
|
|
10271
|
+
request.onerror = () => {
|
|
10272
|
+
reject(request.error);
|
|
10273
|
+
};
|
|
9855
10274
|
});
|
|
9856
10275
|
}
|
|
9857
10276
|
catch (e) {
|
|
@@ -10047,6 +10466,8 @@ class DPoPManager {
|
|
|
10047
10466
|
constructor(storage, cryptoProvider, callbacks) {
|
|
10048
10467
|
this.storage = storage;
|
|
10049
10468
|
this.memoryKeyPair = null;
|
|
10469
|
+
/** Pending key pair promise to prevent concurrent generation race conditions */
|
|
10470
|
+
this.pendingKeyPair = null;
|
|
10050
10471
|
this.cryptoProvider = cryptoProvider || new WebDPoPCryptoProvider();
|
|
10051
10472
|
this.callbacks = callbacks || {};
|
|
10052
10473
|
}
|
|
@@ -10055,8 +10476,9 @@ class DPoPManager {
|
|
|
10055
10476
|
* Useful for detecting if a key regeneration would be needed.
|
|
10056
10477
|
*/
|
|
10057
10478
|
async hasStoredKeys() {
|
|
10058
|
-
if (this.memoryKeyPair)
|
|
10479
|
+
if (this.memoryKeyPair) {
|
|
10059
10480
|
return true;
|
|
10481
|
+
}
|
|
10060
10482
|
const storedPublic = await this.storage.get(DPOP_STORAGE_KEYS.PUBLIC);
|
|
10061
10483
|
const storedPrivate = await this.storage.get(DPOP_STORAGE_KEYS.PRIVATE);
|
|
10062
10484
|
return !!(storedPublic && storedPrivate);
|
|
@@ -10070,8 +10492,35 @@ class DPoPManager {
|
|
|
10070
10492
|
* match refresh token binding" errors.
|
|
10071
10493
|
*/
|
|
10072
10494
|
async ensureKeyPair() {
|
|
10073
|
-
|
|
10495
|
+
// Fast path: already have keys in memory
|
|
10496
|
+
if (this.memoryKeyPair) {
|
|
10497
|
+
return this.memoryKeyPair;
|
|
10498
|
+
}
|
|
10499
|
+
// Prevent concurrent key generation race condition:
|
|
10500
|
+
// If another call is already generating keys, wait for it instead of starting a new one
|
|
10501
|
+
if (this.pendingKeyPair) {
|
|
10502
|
+
return this.pendingKeyPair;
|
|
10503
|
+
}
|
|
10504
|
+
// Create a promise that subsequent callers will wait on
|
|
10505
|
+
this.pendingKeyPair = this.loadOrGenerateKeyPair();
|
|
10506
|
+
try {
|
|
10507
|
+
const keyPair = await this.pendingKeyPair;
|
|
10508
|
+
return keyPair;
|
|
10509
|
+
}
|
|
10510
|
+
finally {
|
|
10511
|
+
// Clear pending promise so future calls can proceed
|
|
10512
|
+
this.pendingKeyPair = null;
|
|
10513
|
+
}
|
|
10514
|
+
}
|
|
10515
|
+
/**
|
|
10516
|
+
* Internal method that actually loads or generates keys.
|
|
10517
|
+
* Called only once per concurrent batch of ensureKeyPair() calls.
|
|
10518
|
+
*/
|
|
10519
|
+
async loadOrGenerateKeyPair() {
|
|
10520
|
+
// Double-check in case keys were set while waiting
|
|
10521
|
+
if (this.memoryKeyPair) {
|
|
10074
10522
|
return this.memoryKeyPair;
|
|
10523
|
+
}
|
|
10075
10524
|
const storedPublic = await this.storage.get(DPOP_STORAGE_KEYS.PUBLIC);
|
|
10076
10525
|
const storedPrivate = await this.storage.get(DPOP_STORAGE_KEYS.PRIVATE);
|
|
10077
10526
|
if (storedPublic && storedPrivate) {
|
|
@@ -10083,18 +10532,23 @@ class DPoPManager {
|
|
|
10083
10532
|
return this.memoryKeyPair;
|
|
10084
10533
|
}
|
|
10085
10534
|
catch (e) {
|
|
10086
|
-
console.warn('[DPoPManager] Corrupted DPoP keys in storage, regenerating
|
|
10535
|
+
console.warn('[DPoPManager] Corrupted DPoP keys in storage, regenerating');
|
|
10087
10536
|
}
|
|
10088
10537
|
}
|
|
10089
10538
|
// Generate new key pair
|
|
10090
10539
|
// IMPORTANT: This invalidates any existing refresh tokens bound to the old keys
|
|
10091
|
-
|
|
10092
|
-
// Adaptation: If storage supports raw objects (like IndexedDB or Native Keychain),
|
|
10540
|
+
// Adaptation: If storage supports raw objects (like Native Keychain on iOS/Android),
|
|
10093
10541
|
// we can generate Non-Extractable keys for maximum security.
|
|
10094
|
-
// If storage is text-only (LocalStorage)
|
|
10542
|
+
// If storage is text-only (LocalStorage) OR IndexedDB (CryptoKey doesn't survive reload),
|
|
10543
|
+
// we must use Extractable keys to serialize them as JWK.
|
|
10544
|
+
//
|
|
10545
|
+
// NOTE: IndexedDB CAN store CryptoKey objects, but they don't survive page reloads -
|
|
10546
|
+
// they come back as empty/corrupted objects. Only native secure storage can persist
|
|
10547
|
+
// non-extractable keys. Frontend should set supportsObjects: false for web storage.
|
|
10095
10548
|
const useHighSecurity = !!this.storage.supportsObjects;
|
|
10549
|
+
const extractable = !useHighSecurity;
|
|
10096
10550
|
const keyPair = await this.cryptoProvider.generateKeyPair({
|
|
10097
|
-
extractable
|
|
10551
|
+
extractable
|
|
10098
10552
|
});
|
|
10099
10553
|
// Save to storage
|
|
10100
10554
|
await this.storage.set(DPOP_STORAGE_KEYS.PUBLIC, keyPair.publicKey);
|
|
@@ -10128,6 +10582,20 @@ class DPoPManager {
|
|
|
10128
10582
|
}
|
|
10129
10583
|
return this.cryptoProvider.signProof(payload, keyPair);
|
|
10130
10584
|
}
|
|
10585
|
+
/**
|
|
10586
|
+
* Generate a short fingerprint of the public key for debugging
|
|
10587
|
+
* This helps identify if the same key is being used across operations
|
|
10588
|
+
*/
|
|
10589
|
+
/* private getPublicKeyFingerprint(publicKey: JsonWebKey): string {
|
|
10590
|
+
try {
|
|
10591
|
+
// Use the 'x' coordinate of the EC key as a fingerprint
|
|
10592
|
+
const x = publicKey.x || '';
|
|
10593
|
+
const y = publicKey.y || '';
|
|
10594
|
+
return `${x.substring(0, 8)}...${y.substring(0, 4)}`;
|
|
10595
|
+
} catch {
|
|
10596
|
+
return 'unknown';
|
|
10597
|
+
}
|
|
10598
|
+
} */
|
|
10131
10599
|
/**
|
|
10132
10600
|
* Clears DPoP keys (e.g. on logout)
|
|
10133
10601
|
*/
|
|
@@ -10176,13 +10644,15 @@ class DefaultAuthProvider {
|
|
|
10176
10644
|
this.tokenManager = new AuthTokenManager(storage);
|
|
10177
10645
|
if (this.dpopEnabled) {
|
|
10178
10646
|
this.dpopManager = new DPoPManager(storage, config.dpop?.cryptoProvider, {
|
|
10179
|
-
// When DPoP keys are regenerated (corrupted/missing),
|
|
10180
|
-
// bound to the old keys
|
|
10181
|
-
//
|
|
10647
|
+
// When DPoP keys are regenerated (corrupted/missing), auth tokens
|
|
10648
|
+
// bound to the old keys become invalid. Clear only auth tokens, NOT DPoP keys.
|
|
10649
|
+
// Access token has cnf claim binding it to the old public key.
|
|
10650
|
+
// Refresh token has DPoP binding to the old key pair.
|
|
10651
|
+
// NOTE: Do NOT call clearAllTokens() here - it would wipe the freshly saved DPoP keys!
|
|
10182
10652
|
onKeysRegenerated: () => {
|
|
10183
|
-
|
|
10184
|
-
this.tokenManager.
|
|
10185
|
-
console.warn('[DefaultAuthProvider] Failed to clear
|
|
10653
|
+
// Clear only auth tokens, preserve DPoP keys (they were just regenerated and saved)
|
|
10654
|
+
this.tokenManager.clearAuthTokens().catch(err => {
|
|
10655
|
+
console.warn('[DefaultAuthProvider] Failed to clear tokens after key regeneration:', err);
|
|
10186
10656
|
});
|
|
10187
10657
|
}
|
|
10188
10658
|
});
|
|
@@ -10324,6 +10794,41 @@ class DefaultAuthProvider {
|
|
|
10324
10794
|
return null;
|
|
10325
10795
|
}
|
|
10326
10796
|
}
|
|
10797
|
+
/**
|
|
10798
|
+
* Store the provider token (Firebase JWT, Auth0 token, etc.)
|
|
10799
|
+
* Used for session recovery when refresh tokens expire.
|
|
10800
|
+
*
|
|
10801
|
+
* NOTE: This is called AUTOMATICALLY by loginWithToken(), so you typically
|
|
10802
|
+
* don't need to call this manually. The only reason to call this is if:
|
|
10803
|
+
*
|
|
10804
|
+
* 1. Your auth provider (Firebase, Auth0) refreshes tokens in the background
|
|
10805
|
+
* 2. You want to proactively update the stored token for better recovery chances
|
|
10806
|
+
*
|
|
10807
|
+
* The stored provider token is used as a fallback when:
|
|
10808
|
+
* - PERS refresh token expires (e.g., after 30 days of inactivity)
|
|
10809
|
+
* - SDK attempts recovery using the stored provider token
|
|
10810
|
+
* - If provider token is still valid → session recovered automatically
|
|
10811
|
+
* - If provider token is also expired → user must re-authenticate
|
|
10812
|
+
*
|
|
10813
|
+
* @example
|
|
10814
|
+
* // Optional: Update stored token when Firebase refreshes it
|
|
10815
|
+
* firebase.auth().onIdTokenChanged(async (user) => {
|
|
10816
|
+
* if (user) {
|
|
10817
|
+
* const freshToken = await user.getIdToken();
|
|
10818
|
+
* await sdk.auth.setProviderToken(freshToken);
|
|
10819
|
+
* }
|
|
10820
|
+
* });
|
|
10821
|
+
*/
|
|
10822
|
+
async setProviderToken(token) {
|
|
10823
|
+
await this.tokenManager.setProviderToken(token);
|
|
10824
|
+
}
|
|
10825
|
+
/**
|
|
10826
|
+
* Get the stored provider token (Firebase JWT, Auth0 token, etc.)
|
|
10827
|
+
* Used internally for session recovery when refresh tokens expire.
|
|
10828
|
+
*/
|
|
10829
|
+
async getProviderToken() {
|
|
10830
|
+
return this.tokenManager.getProviderToken();
|
|
10831
|
+
}
|
|
10327
10832
|
}
|
|
10328
10833
|
|
|
10329
10834
|
/**
|
|
@@ -10339,7 +10844,7 @@ class DefaultAuthProvider {
|
|
|
10339
10844
|
/** SDK package name */
|
|
10340
10845
|
const SDK_NAME = "@explorins/pers-sdk";
|
|
10341
10846
|
/** SDK version - injected from package.json at build time */
|
|
10342
|
-
const SDK_VERSION = "2.1.
|
|
10847
|
+
const SDK_VERSION = "2.1.37";
|
|
10343
10848
|
/** Full SDK identifier for headers */
|
|
10344
10849
|
const SDK_USER_AGENT = `${SDK_NAME}/${SDK_VERSION}`;
|
|
10345
10850
|
|
|
@@ -10488,7 +10993,9 @@ class PersApiClient {
|
|
|
10488
10993
|
// Handle 401 errors centrally through AuthService
|
|
10489
10994
|
if (status === 401) {
|
|
10490
10995
|
const backendError = ErrorUtils.extractBackendErrorDetails(error);
|
|
10491
|
-
// Check for
|
|
10996
|
+
// Check for FATAL auth error CODES (not just 401 status)
|
|
10997
|
+
// Fatal codes: REFRESH_TOKEN_EXPIRED, TOKEN_REVOKED, etc. → immediate logout
|
|
10998
|
+
// Non-fatal codes: TOKEN_EXPIRED → try refresh first
|
|
10492
10999
|
if (this.authService.isFatalAuthError(backendError.code)) {
|
|
10493
11000
|
// Definitive auth failure - no retry, session is invalid
|
|
10494
11001
|
await this.authService.handleAuthFailure();
|
|
@@ -10548,9 +11055,9 @@ class PersApiClient {
|
|
|
10548
11055
|
}); */
|
|
10549
11056
|
this._events?.emitError({
|
|
10550
11057
|
domain: errorDetails.domain || 'external',
|
|
10551
|
-
type:
|
|
10552
|
-
userMessage: errorDetails.message || errorMessage,
|
|
10553
|
-
code: errorDetails.code,
|
|
11058
|
+
type: 'api_error', // Event type discriminator (fixed value)
|
|
11059
|
+
userMessage: errorDetails.message || errorMessage,
|
|
11060
|
+
code: errorDetails.code, // Actual error code from backend
|
|
10554
11061
|
details: {
|
|
10555
11062
|
endpoint,
|
|
10556
11063
|
method,
|
|
@@ -12186,6 +12693,107 @@ class TokenManager {
|
|
|
12186
12693
|
async getTokenByContract(contractAddress, contractTokenId = null) {
|
|
12187
12694
|
return this.tokenService.getTokenByContractAddress(contractAddress, contractTokenId);
|
|
12188
12695
|
}
|
|
12696
|
+
/**
|
|
12697
|
+
* Get all token metadata with filtering and pagination
|
|
12698
|
+
*
|
|
12699
|
+
* Retrieves token metadata (rewards/stamps) with server-side filtering and pagination.
|
|
12700
|
+
* Useful for displaying rewards (ERC1155) or stamps (ERC721) in admin tables.
|
|
12701
|
+
* Non-admin users always get active metadata only.
|
|
12702
|
+
*
|
|
12703
|
+
* @param options - Filter and pagination options
|
|
12704
|
+
* @returns Promise resolving to paginated token metadata
|
|
12705
|
+
*
|
|
12706
|
+
* @example Get paginated rewards
|
|
12707
|
+
* ```typescript
|
|
12708
|
+
* const rewards = await sdk.tokens.getTokenMetadata({
|
|
12709
|
+
* tokenType: 'ERC1155',
|
|
12710
|
+
* active: true,
|
|
12711
|
+
* page: 1,
|
|
12712
|
+
* limit: 10,
|
|
12713
|
+
* sortBy: 'name',
|
|
12714
|
+
* sortOrder: 'ASC'
|
|
12715
|
+
* });
|
|
12716
|
+
*
|
|
12717
|
+
* console.log(`Page 1 of ${rewards.pagination.pages}`);
|
|
12718
|
+
* rewards.data.forEach(r => console.log(r.name));
|
|
12719
|
+
* ```
|
|
12720
|
+
*
|
|
12721
|
+
* @example Get stamps with search
|
|
12722
|
+
* ```typescript
|
|
12723
|
+
* const stamps = await sdk.tokens.getTokenMetadata({
|
|
12724
|
+
* tokenType: 'ERC721',
|
|
12725
|
+
* search: 'gold',
|
|
12726
|
+
* limit: 25
|
|
12727
|
+
* });
|
|
12728
|
+
* ```
|
|
12729
|
+
*/
|
|
12730
|
+
async getTokenMetadata(options) {
|
|
12731
|
+
return this.tokenService.getTokenMetadata(options);
|
|
12732
|
+
}
|
|
12733
|
+
/**
|
|
12734
|
+
* Get rewards (ERC1155 token metadata) with filtering and pagination
|
|
12735
|
+
*
|
|
12736
|
+
* Convenience method for fetching reward metadata. Rewards are ERC1155 tokens
|
|
12737
|
+
* that represent collectible items, vouchers, or other redeemable rewards.
|
|
12738
|
+
*
|
|
12739
|
+
* @param options - Filter and pagination options (tokenType is set automatically)
|
|
12740
|
+
* @returns Promise resolving to paginated reward metadata
|
|
12741
|
+
*
|
|
12742
|
+
* @example Get active rewards
|
|
12743
|
+
* ```typescript
|
|
12744
|
+
* const rewards = await sdk.tokens.getRewards({
|
|
12745
|
+
* active: true,
|
|
12746
|
+
* limit: 10
|
|
12747
|
+
* });
|
|
12748
|
+
*
|
|
12749
|
+
* console.log(`Found ${rewards.pagination.total} rewards`);
|
|
12750
|
+
* rewards.data.forEach(r => console.log(`${r.name}: ${r.description}`));
|
|
12751
|
+
* ```
|
|
12752
|
+
*
|
|
12753
|
+
* @example Search rewards
|
|
12754
|
+
* ```typescript
|
|
12755
|
+
* const discountRewards = await sdk.tokens.getRewards({
|
|
12756
|
+
* search: 'discount',
|
|
12757
|
+
* sortBy: 'name',
|
|
12758
|
+
* sortOrder: 'ASC'
|
|
12759
|
+
* });
|
|
12760
|
+
* ```
|
|
12761
|
+
*/
|
|
12762
|
+
async getRewards(options) {
|
|
12763
|
+
return this.tokenService.getRewards(options);
|
|
12764
|
+
}
|
|
12765
|
+
/**
|
|
12766
|
+
* Get stamps (ERC721 token metadata) with filtering and pagination
|
|
12767
|
+
*
|
|
12768
|
+
* Convenience method for fetching stamp metadata. Stamps are ERC721 tokens
|
|
12769
|
+
* that represent achievements, badges, or collectible status markers.
|
|
12770
|
+
*
|
|
12771
|
+
* @param options - Filter and pagination options (tokenType is set automatically)
|
|
12772
|
+
* @returns Promise resolving to paginated stamp metadata
|
|
12773
|
+
*
|
|
12774
|
+
* @example Get active stamps
|
|
12775
|
+
* ```typescript
|
|
12776
|
+
* const stamps = await sdk.tokens.getStamps({
|
|
12777
|
+
* active: true,
|
|
12778
|
+
* limit: 10
|
|
12779
|
+
* });
|
|
12780
|
+
*
|
|
12781
|
+
* console.log(`Found ${stamps.pagination.total} stamps`);
|
|
12782
|
+
* stamps.data.forEach(s => console.log(`${s.name}: ${s.description}`));
|
|
12783
|
+
* ```
|
|
12784
|
+
*
|
|
12785
|
+
* @example Filter stamps by tags
|
|
12786
|
+
* ```typescript
|
|
12787
|
+
* const goldStamps = await sdk.tokens.getStamps({
|
|
12788
|
+
* tags: ['gold', 'premium'],
|
|
12789
|
+
* sortBy: 'createdAt',
|
|
12790
|
+
* sortOrder: 'DESC'
|
|
12791
|
+
* });
|
|
12792
|
+
* ```
|
|
12793
|
+
*/
|
|
12794
|
+
async getStamps(options) {
|
|
12795
|
+
return this.tokenService.getStamps(options);
|
|
12796
|
+
}
|
|
12189
12797
|
/**
|
|
12190
12798
|
* Admin: Create new token
|
|
12191
12799
|
*
|
|
@@ -14090,6 +14698,52 @@ class RedemptionManager {
|
|
|
14090
14698
|
async getRedemptions(options) {
|
|
14091
14699
|
return this.redemptionService.getRedemptions(options);
|
|
14092
14700
|
}
|
|
14701
|
+
/**
|
|
14702
|
+
* Get a specific redemption by ID
|
|
14703
|
+
*
|
|
14704
|
+
* Retrieves detailed information about a specific redemption offer.
|
|
14705
|
+
* Useful when you already have the redemption ID and need full details.
|
|
14706
|
+
*
|
|
14707
|
+
* @param id - Redemption UUID
|
|
14708
|
+
* @param include - Optional relations to include: 'redeemCount'
|
|
14709
|
+
* @returns Promise resolving to the redemption
|
|
14710
|
+
*
|
|
14711
|
+
* @example Basic usage
|
|
14712
|
+
* ```typescript
|
|
14713
|
+
* const redemption = await sdk.redemptions.getRedemptionById('7baf4d39-0bd6-45ca-9c66-8c0d655767c3');
|
|
14714
|
+
* console.log(redemption.name);
|
|
14715
|
+
* console.log(redemption.description);
|
|
14716
|
+
* ```
|
|
14717
|
+
*
|
|
14718
|
+
* @example With redeem count
|
|
14719
|
+
* ```typescript
|
|
14720
|
+
* const redemption = await sdk.redemptions.getRedemptionById(
|
|
14721
|
+
* 'redemption-123',
|
|
14722
|
+
* ['redeemCount']
|
|
14723
|
+
* );
|
|
14724
|
+
* console.log(`${redemption.name} has been redeemed ${redemption.redeemCount} times`);
|
|
14725
|
+
* ```
|
|
14726
|
+
*
|
|
14727
|
+
* @example Get redemption details for display
|
|
14728
|
+
* ```typescript
|
|
14729
|
+
* const redemption = await sdk.redemptions.getRedemptionById(id);
|
|
14730
|
+
*
|
|
14731
|
+
* console.log('Redemption Details:');
|
|
14732
|
+
* console.log(`Name: ${redemption.name}`);
|
|
14733
|
+
* console.log(`Description: ${redemption.description}`);
|
|
14734
|
+
* console.log(`Active: ${redemption.isActive}`);
|
|
14735
|
+
*
|
|
14736
|
+
* if (redemption.tokenUnits?.length) {
|
|
14737
|
+
* console.log('Cost:');
|
|
14738
|
+
* redemption.tokenUnits.forEach(unit => {
|
|
14739
|
+
* console.log(` ${unit.amount} ${unit.token.symbol}`);
|
|
14740
|
+
* });
|
|
14741
|
+
* }
|
|
14742
|
+
* ```
|
|
14743
|
+
*/
|
|
14744
|
+
async getRedemptionById(id, include) {
|
|
14745
|
+
return this.redemptionService.getRedemptionById(id, include);
|
|
14746
|
+
}
|
|
14093
14747
|
/**
|
|
14094
14748
|
* Get available redemption types
|
|
14095
14749
|
*
|
|
@@ -16615,6 +17269,55 @@ class AnalyticsManager {
|
|
|
16615
17269
|
async getCampaignClaimAnalytics(request) {
|
|
16616
17270
|
return this.analyticsService.getCampaignClaimAnalytics(request);
|
|
16617
17271
|
}
|
|
17272
|
+
/**
|
|
17273
|
+
* Get redemption redeem analytics with aggregation
|
|
17274
|
+
*
|
|
17275
|
+
* Retrieves aggregated redemption redeem data with flexible grouping and metrics.
|
|
17276
|
+
* Perfect for charts, dashboards, and redeem pattern analysis across redemptions,
|
|
17277
|
+
* businesses, and time periods.
|
|
17278
|
+
*
|
|
17279
|
+
* @param request - Analytics request with filters, groupBy, and metrics
|
|
17280
|
+
* @returns Promise resolving to redemption redeem analytics data
|
|
17281
|
+
*
|
|
17282
|
+
* @example Redeems per redemption
|
|
17283
|
+
* ```typescript
|
|
17284
|
+
* const analytics = await sdk.analytics.getRedemptionRedeemAnalytics({
|
|
17285
|
+
* groupBy: ['redemptionId'],
|
|
17286
|
+
* metrics: ['count'],
|
|
17287
|
+
* sortBy: 'count',
|
|
17288
|
+
* sortOrder: 'DESC',
|
|
17289
|
+
* limit: 10
|
|
17290
|
+
* });
|
|
17291
|
+
*
|
|
17292
|
+
* console.log('Top redemptions by redeems:', analytics.results);
|
|
17293
|
+
* ```
|
|
17294
|
+
*
|
|
17295
|
+
* @example Redeems by status over time
|
|
17296
|
+
* ```typescript
|
|
17297
|
+
* const analytics = await sdk.analytics.getRedemptionRedeemAnalytics({
|
|
17298
|
+
* groupBy: ['month', 'status'],
|
|
17299
|
+
* metrics: ['count'],
|
|
17300
|
+
* sortBy: 'month',
|
|
17301
|
+
* sortOrder: 'DESC',
|
|
17302
|
+
* startDate: new Date('2026-01-01'),
|
|
17303
|
+
* endDate: new Date('2026-12-31')
|
|
17304
|
+
* });
|
|
17305
|
+
* ```
|
|
17306
|
+
*
|
|
17307
|
+
* @example Redeems by business
|
|
17308
|
+
* ```typescript
|
|
17309
|
+
* const analytics = await sdk.analytics.getRedemptionRedeemAnalytics({
|
|
17310
|
+
* filters: { status: 'COMPLETED' },
|
|
17311
|
+
* groupBy: ['businessId'],
|
|
17312
|
+
* metrics: ['count'],
|
|
17313
|
+
* sortBy: 'count',
|
|
17314
|
+
* sortOrder: 'DESC'
|
|
17315
|
+
* });
|
|
17316
|
+
* ```
|
|
17317
|
+
*/
|
|
17318
|
+
async getRedemptionRedeemAnalytics(request) {
|
|
17319
|
+
return this.analyticsService.getRedemptionRedeemAnalytics(request);
|
|
17320
|
+
}
|
|
16618
17321
|
/**
|
|
16619
17322
|
* Get user analytics with engagement metrics
|
|
16620
17323
|
*
|
|
@@ -20170,7 +20873,7 @@ const PersSDKProvider = ({ children, config }) => {
|
|
|
20170
20873
|
setIsAuthenticated(true);
|
|
20171
20874
|
}
|
|
20172
20875
|
}
|
|
20173
|
-
if (event.type === 'logout_success' || event.type === 'session_restoration_failed'
|
|
20876
|
+
if (event.type === 'logout_success' || event.type === 'session_restoration_failed') {
|
|
20174
20877
|
setUser(null);
|
|
20175
20878
|
setIsAuthenticated(false);
|
|
20176
20879
|
}
|
|
@@ -20729,6 +21432,99 @@ const useTokens = () => {
|
|
|
20729
21432
|
throw error;
|
|
20730
21433
|
}
|
|
20731
21434
|
}, [sdk, isInitialized]);
|
|
21435
|
+
/**
|
|
21436
|
+
* Get all token metadata with filtering and pagination
|
|
21437
|
+
*
|
|
21438
|
+
* Retrieves token metadata (rewards/stamps) with server-side filtering.
|
|
21439
|
+
* Use tokenType: 'ERC1155' for rewards, 'ERC721' for stamps.
|
|
21440
|
+
*
|
|
21441
|
+
* @param options - Filter and pagination options
|
|
21442
|
+
* @returns Promise resolving to paginated token metadata
|
|
21443
|
+
* @throws Error if SDK is not initialized
|
|
21444
|
+
*
|
|
21445
|
+
* @example
|
|
21446
|
+
* ```typescript
|
|
21447
|
+
* const { getTokenMetadata } = useTokens();
|
|
21448
|
+
* const rewards = await getTokenMetadata({
|
|
21449
|
+
* tokenType: 'ERC1155',
|
|
21450
|
+
* active: true,
|
|
21451
|
+
* limit: 10
|
|
21452
|
+
* });
|
|
21453
|
+
* ```
|
|
21454
|
+
*/
|
|
21455
|
+
const getTokenMetadata = react.useCallback(async (options) => {
|
|
21456
|
+
if (!isInitialized || !sdk) {
|
|
21457
|
+
throw new Error('SDK not initialized. Call initialize() first.');
|
|
21458
|
+
}
|
|
21459
|
+
try {
|
|
21460
|
+
const result = await sdk.tokens.getTokenMetadata(options);
|
|
21461
|
+
return result;
|
|
21462
|
+
}
|
|
21463
|
+
catch (error) {
|
|
21464
|
+
console.error('Failed to fetch token metadata:', error);
|
|
21465
|
+
throw error;
|
|
21466
|
+
}
|
|
21467
|
+
}, [sdk, isInitialized]);
|
|
21468
|
+
/**
|
|
21469
|
+
* Get rewards (ERC1155 token metadata) with filtering and pagination
|
|
21470
|
+
*
|
|
21471
|
+
* Convenience method for fetching reward metadata. Rewards are ERC1155 tokens
|
|
21472
|
+
* that represent collectible items, vouchers, or other redeemable rewards.
|
|
21473
|
+
*
|
|
21474
|
+
* @param options - Filter and pagination options
|
|
21475
|
+
* @returns Promise resolving to paginated reward metadata
|
|
21476
|
+
* @throws Error if SDK is not initialized
|
|
21477
|
+
*
|
|
21478
|
+
* @example
|
|
21479
|
+
* ```typescript
|
|
21480
|
+
* const { getRewards } = useTokens();
|
|
21481
|
+
* const rewards = await getRewards({ active: true, limit: 10 });
|
|
21482
|
+
* console.log(`Found ${rewards.pagination.total} rewards`);
|
|
21483
|
+
* ```
|
|
21484
|
+
*/
|
|
21485
|
+
const getRewards = react.useCallback(async (options) => {
|
|
21486
|
+
if (!isInitialized || !sdk) {
|
|
21487
|
+
throw new Error('SDK not initialized. Call initialize() first.');
|
|
21488
|
+
}
|
|
21489
|
+
try {
|
|
21490
|
+
const result = await sdk.tokens.getRewards(options);
|
|
21491
|
+
return result;
|
|
21492
|
+
}
|
|
21493
|
+
catch (error) {
|
|
21494
|
+
console.error('Failed to fetch rewards:', error);
|
|
21495
|
+
throw error;
|
|
21496
|
+
}
|
|
21497
|
+
}, [sdk, isInitialized]);
|
|
21498
|
+
/**
|
|
21499
|
+
* Get stamps (ERC721 token metadata) with filtering and pagination
|
|
21500
|
+
*
|
|
21501
|
+
* Convenience method for fetching stamp metadata. Stamps are ERC721 tokens
|
|
21502
|
+
* that represent achievements, badges, or collectible status markers.
|
|
21503
|
+
*
|
|
21504
|
+
* @param options - Filter and pagination options
|
|
21505
|
+
* @returns Promise resolving to paginated stamp metadata
|
|
21506
|
+
* @throws Error if SDK is not initialized
|
|
21507
|
+
*
|
|
21508
|
+
* @example
|
|
21509
|
+
* ```typescript
|
|
21510
|
+
* const { getStamps } = useTokens();
|
|
21511
|
+
* const stamps = await getStamps({ active: true, limit: 10 });
|
|
21512
|
+
* console.log(`Found ${stamps.pagination.total} stamps`);
|
|
21513
|
+
* ```
|
|
21514
|
+
*/
|
|
21515
|
+
const getStamps = react.useCallback(async (options) => {
|
|
21516
|
+
if (!isInitialized || !sdk) {
|
|
21517
|
+
throw new Error('SDK not initialized. Call initialize() first.');
|
|
21518
|
+
}
|
|
21519
|
+
try {
|
|
21520
|
+
const result = await sdk.tokens.getStamps(options);
|
|
21521
|
+
return result;
|
|
21522
|
+
}
|
|
21523
|
+
catch (error) {
|
|
21524
|
+
console.error('Failed to fetch stamps:', error);
|
|
21525
|
+
throw error;
|
|
21526
|
+
}
|
|
21527
|
+
}, [sdk, isInitialized]);
|
|
20732
21528
|
return {
|
|
20733
21529
|
getTokens,
|
|
20734
21530
|
getActiveCreditToken,
|
|
@@ -20736,6 +21532,9 @@ const useTokens = () => {
|
|
|
20736
21532
|
getTokenTypes,
|
|
20737
21533
|
getStatusTokens,
|
|
20738
21534
|
getTokenByContract,
|
|
21535
|
+
getTokenMetadata,
|
|
21536
|
+
getRewards,
|
|
21537
|
+
getStamps,
|
|
20739
21538
|
isAvailable: isInitialized && !!sdk?.tokens,
|
|
20740
21539
|
};
|
|
20741
21540
|
};
|
|
@@ -43972,6 +44771,25 @@ const useRedemptions = () => {
|
|
|
43972
44771
|
/**
|
|
43973
44772
|
* Redeem a redemption offer with optional dynamic context
|
|
43974
44773
|
*
|
|
44774
|
+
* ## When to use `redeem()` vs `createTransaction()`
|
|
44775
|
+
*
|
|
44776
|
+
* | Use Case | Method | Auto-Signs |
|
|
44777
|
+
* |----------|--------|------------|
|
|
44778
|
+
* | **Voucher redemption** | `redeem()` | ✅ Yes |
|
|
44779
|
+
* | **Reward redemption** | `redeem()` | ✅ Yes |
|
|
44780
|
+
* | **Direct token transfer** | `createTransaction()` | ⚠️ Depends |
|
|
44781
|
+
* | **POS burn with QR** | `buildPOSBurnRequest()` | ❌ Manual |
|
|
44782
|
+
*
|
|
44783
|
+
* **Use `redeem()` for voucher/reward redemptions.** It automatically:
|
|
44784
|
+
* 1. Creates redemption record
|
|
44785
|
+
* 2. Triggers passkey/biometric prompt
|
|
44786
|
+
* 3. Signs the transaction
|
|
44787
|
+
* 4. Submits to blockchain
|
|
44788
|
+
* 5. Returns result with `transactionHash`
|
|
44789
|
+
*
|
|
44790
|
+
* **No admin approval required** - The "PENDING" state is brief (seconds).
|
|
44791
|
+
* The user's passkey signature IS the approval.
|
|
44792
|
+
*
|
|
43975
44793
|
* Supports dynamic context for personalized NFT generation when the redemption
|
|
43976
44794
|
* issues ERC721 tokens. Context can include validity dates, AI prompt placeholders,
|
|
43977
44795
|
* and custom metadata.
|
|
@@ -43980,11 +44798,21 @@ const useRedemptions = () => {
|
|
|
43980
44798
|
* @param options - Optional settings
|
|
43981
44799
|
* @param options.context - Dynamic context for NFT personalization (validity dates, AI prompts)
|
|
43982
44800
|
* @param options.onStatusUpdate - Callback for transaction signing status updates
|
|
43983
|
-
* @returns Promise resolving to redemption result
|
|
44801
|
+
* @returns Promise resolving to redemption result with `transactionHash` on success
|
|
43984
44802
|
*
|
|
43985
|
-
* @example Basic redemption
|
|
44803
|
+
* @example Basic redemption (most common)
|
|
43986
44804
|
* ```typescript
|
|
43987
|
-
* const
|
|
44805
|
+
* const { redeem } = useRedemptions();
|
|
44806
|
+
*
|
|
44807
|
+
* const result = await redeem('redemption-123', {
|
|
44808
|
+
* onStatusUpdate: (status, message) => {
|
|
44809
|
+
* console.log(`Status: ${status} - ${message}`);
|
|
44810
|
+
* }
|
|
44811
|
+
* });
|
|
44812
|
+
*
|
|
44813
|
+
* if (result.transactionHash) {
|
|
44814
|
+
* console.log('Redemption complete!', result.transactionHash);
|
|
44815
|
+
* }
|
|
43988
44816
|
* ```
|
|
43989
44817
|
*
|
|
43990
44818
|
* @example Redemption with dynamic context (personalized NFT)
|
|
@@ -44015,6 +44843,8 @@ const useRedemptions = () => {
|
|
|
44015
44843
|
* }
|
|
44016
44844
|
* });
|
|
44017
44845
|
* ```
|
|
44846
|
+
*
|
|
44847
|
+
* @see {@link useTransactions.createTransaction | createTransaction()} for direct transfers without redemption flow
|
|
44018
44848
|
*/
|
|
44019
44849
|
const redeem = react.useCallback(async (redemptionId, options) => {
|
|
44020
44850
|
if (!isInitialized || !sdk) {
|
|
@@ -44164,8 +44994,43 @@ const useRedemptions = () => {
|
|
|
44164
44994
|
throw error;
|
|
44165
44995
|
}
|
|
44166
44996
|
}, [sdk, isInitialized]);
|
|
44997
|
+
/**
|
|
44998
|
+
* Get a specific redemption by ID
|
|
44999
|
+
*
|
|
45000
|
+
* Retrieves detailed information about a specific redemption offer.
|
|
45001
|
+
*
|
|
45002
|
+
* @param id - Redemption UUID
|
|
45003
|
+
* @param include - Optional relations to include: 'redeemCount'
|
|
45004
|
+
* @returns Promise resolving to the redemption
|
|
45005
|
+
*
|
|
45006
|
+
* @example Basic usage
|
|
45007
|
+
* ```typescript
|
|
45008
|
+
* const redemption = await getRedemptionById('7baf4d39-0bd6-45ca-9c66-8c0d655767c3');
|
|
45009
|
+
* console.log(redemption.name);
|
|
45010
|
+
* ```
|
|
45011
|
+
*
|
|
45012
|
+
* @example With redeem count
|
|
45013
|
+
* ```typescript
|
|
45014
|
+
* const redemption = await getRedemptionById('redemption-123', ['redeemCount']);
|
|
45015
|
+
* console.log(`${redemption.name} has been redeemed ${redemption.redeemCount} times`);
|
|
45016
|
+
* ```
|
|
45017
|
+
*/
|
|
45018
|
+
const getRedemptionById = react.useCallback(async (id, include) => {
|
|
45019
|
+
if (!isInitialized || !sdk) {
|
|
45020
|
+
throw new Error('SDK not initialized. Call initialize() first.');
|
|
45021
|
+
}
|
|
45022
|
+
try {
|
|
45023
|
+
const result = await sdk.redemptions.getRedemptionById(id, include);
|
|
45024
|
+
return result;
|
|
45025
|
+
}
|
|
45026
|
+
catch (error) {
|
|
45027
|
+
console.error('Failed to fetch redemption by ID:', error);
|
|
45028
|
+
throw error;
|
|
45029
|
+
}
|
|
45030
|
+
}, [sdk, isInitialized]);
|
|
44167
45031
|
return {
|
|
44168
45032
|
getRedemptions,
|
|
45033
|
+
getRedemptionById,
|
|
44169
45034
|
getUserRedemptions,
|
|
44170
45035
|
redeem,
|
|
44171
45036
|
getRedemptionTypes,
|
|
@@ -45797,6 +46662,7 @@ exports.DonationService = DonationService;
|
|
|
45797
46662
|
exports.EntityTypes = EntityTypes;
|
|
45798
46663
|
exports.ErrorDomains = ErrorDomains;
|
|
45799
46664
|
exports.ErrorUtils = ErrorUtils;
|
|
46665
|
+
exports.FATAL_AUTH_CODES = FATAL_AUTH_CODES;
|
|
45800
46666
|
exports.FileApi = FileApi;
|
|
45801
46667
|
exports.FileManager = FileManager;
|
|
45802
46668
|
exports.FileService = FileService;
|
|
@@ -45902,6 +46768,7 @@ exports.initializeReactNativePolyfills = initializeReactNativePolyfills;
|
|
|
45902
46768
|
exports.isConnectedMessage = isConnectedMessage;
|
|
45903
46769
|
exports.isErrorMessage = isErrorMessage;
|
|
45904
46770
|
exports.isEventMessage = isEventMessage;
|
|
46771
|
+
exports.isFatalAuthErrorInMessage = isFatalAuthErrorInMessage;
|
|
45905
46772
|
exports.isPaginatedResponse = isPaginatedResponse;
|
|
45906
46773
|
exports.isPingMessage = isPingMessage;
|
|
45907
46774
|
exports.isServerMessage = isServerMessage;
|