Skip to content

Commit d78f0e4

Browse files
Fix Wasm incompatibility in web error processing (#1651)
Replace `is JSObject` runtime check with Wasm-safe `isA<JSObject>()` in `_processError`. The `is` operator on JS interop types is unreliable under Wasm compilation due to type erasure differences between JS and Wasm targets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 40cb9b0 commit d78f0e4

2 files changed

Lines changed: 67 additions & 69 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
### ✨ New Features
44
* Add setAppstackAttributionParams API (#1671) via Rick (@rickvdl)
55

6+
### 🐞 Bugfixes
7+
* Fix Wasm incompatibility: replace `is JSObject` runtime check with `isA<JSObject>()` in web error processing (#1651)
8+
69
## 9.13.2
710
## RevenueCat SDK
811
### 📦 Dependency Updates

lib/web/purchases_flutter_web.dart

Lines changed: 64 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import '../purchases_flutter.dart';
1111

1212
class PurchasesFlutterPlugin {
1313
static final _unknownErrorCode = '${PurchasesErrorCode.unknownError.index}';
14-
static final _configurationErrorCode = '${PurchasesErrorCode.configurationError.index}';
14+
static final _configurationErrorCode =
15+
'${PurchasesErrorCode.configurationError.index}';
1516
static const _purchasesHybridMappingsVersion = '17.50.0';
1617
static const _platformName = 'flutter';
1718
static const _pluginVersion = '9.14.0';
@@ -26,9 +27,10 @@ class PurchasesFlutterPlugin {
2627
}
2728
_initCompleter = Completer<void>();
2829

29-
final script = HTMLScriptElement()
30-
..type = 'text/javascript'
31-
..text = '''
30+
final script =
31+
HTMLScriptElement()
32+
..type = 'text/javascript'
33+
..text = '''
3234
window.after_rc_load_callback = async (callback) => {
3335
callback(await import("$_purchasesHybridMappingsUrl"));
3436
};
@@ -40,7 +42,7 @@ class PurchasesFlutterPlugin {
4042
"The current document doesn't have a head element which is required to insert a script.",
4143
);
4244
}
43-
45+
4446
head.append(script);
4547

4648
globalContext.callMethod(
@@ -71,8 +73,8 @@ class PurchasesFlutterPlugin {
7173
Future<dynamic> handleMethodCall(MethodCall call) async {
7274
if (_initCompleter == null) {
7375
throw PlatformException(
74-
code: _unknownErrorCode,
75-
message: 'Purchases SDK not loaded on method call: ${call.method}',
76+
code: _unknownErrorCode,
77+
message: 'Purchases SDK not loaded on method call: ${call.method}',
7678
);
7779
}
7880
await _initCompleter?.future;
@@ -183,7 +185,7 @@ class PurchasesFlutterPlugin {
183185
};
184186
final appUserId = arguments['appUserId'] as String?;
185187

186-
if(appUserId != null && appUserId.isNotEmpty) {
188+
if (appUserId != null && appUserId.isNotEmpty) {
187189
options['appUserId'] = appUserId;
188190
}
189191

@@ -218,8 +220,8 @@ class PurchasesFlutterPlugin {
218220
) async {
219221
final placementIdentifier = arguments['placementIdentifier'] as String;
220222
return await _getNullableMapFromInstanceMethod(
221-
'getCurrentOfferingForPlacement',
222-
[placementIdentifier],
223+
'getCurrentOfferingForPlacement',
224+
[placementIdentifier],
223225
);
224226
}
225227

@@ -248,9 +250,9 @@ class PurchasesFlutterPlugin {
248250
}
249251

250252
Future<Map<String, dynamic>> _restorePurchases() async =>
251-
// Web SDK doesn't support restore purchases, but returns current customer info
252-
// to match behavior with other platforms
253-
await _getCustomerInfo();
253+
// Web SDK doesn't support restore purchases, but returns current customer info
254+
// to match behavior with other platforms
255+
await _getCustomerInfo();
254256

255257
Future<void> _close() async {
256258
_getInstance().callMethod('close'.toJS);
@@ -271,18 +273,16 @@ class PurchasesFlutterPlugin {
271273
) async {
272274
// Web SDK doesn't support trial/intro eligibility checking
273275
// Return unknown status for all products
274-
final productIdentifiers =
275-
List<String>.from(arguments['productIdentifiers']);
276+
final productIdentifiers = List<String>.from(
277+
arguments['productIdentifiers'],
278+
);
276279
return Map.fromEntries(
277280
productIdentifiers.map(
278-
(id) => MapEntry(
279-
id,
280-
{
281-
'status': 0, // introEligibilityStatusUnknown
282-
'description':
283-
'Trial or intro price eligibility checking not supported on web',
284-
},
285-
),
281+
(id) => MapEntry(id, {
282+
'status': 0, // introEligibilityStatusUnknown
283+
'description':
284+
'Trial or intro price eligibility checking not supported on web',
285+
}),
286286
),
287287
);
288288
}
@@ -319,29 +319,25 @@ class PurchasesFlutterPlugin {
319319
}
320320

321321
Future<Map<String, dynamic>> _getVirtualCurrencies() async =>
322-
await _getMapFromInstanceMethod('getVirtualCurrencies', []);
322+
await _getMapFromInstanceMethod('getVirtualCurrencies', []);
323323

324324
Future<void> _invalidateVirtualCurrenciesCache() async {
325325
_getInstance().callMethod('invalidateVirtualCurrenciesCache'.toJS);
326326
}
327327

328328
Future<Map<String, dynamic>?> _getCachedVirtualCurrencies() async =>
329329
_getNullableMapFromInstanceSyncMethod('getCachedVirtualCurrencies');
330-
330+
331331
// Helper functions to handle JS interop
332332

333-
Object? _callStaticMethod(
334-
String methodName,
335-
List<dynamic> args,
336-
) {
333+
Object? _callStaticMethod(String methodName, List<dynamic> args) {
337334
final purchasesStatic = _getStaticPurchasesCommon();
338335
final processedArgs = _processArgs(args);
339336

340337
try {
341-
return purchasesStatic.callMethodVarArgs(
342-
methodName.toJS,
343-
processedArgs,
344-
).dartify();
338+
return purchasesStatic
339+
.callMethodVarArgs(methodName.toJS, processedArgs)
340+
.dartify();
345341
} catch (e) {
346342
throw PlatformException(
347343
code: _unknownErrorCode,
@@ -350,10 +346,7 @@ class PurchasesFlutterPlugin {
350346
}
351347
}
352348

353-
JSObject _callInstanceMethod(
354-
String methodName,
355-
List<dynamic> args,
356-
) {
349+
JSObject _callInstanceMethod(String methodName, List<dynamic> args) {
357350
final purchases = _getInstance();
358351
final jsArgs = _processArgs(args);
359352
return purchases.callMethodVarArgs(methodName.toJS, jsArgs);
@@ -373,7 +366,8 @@ class PurchasesFlutterPlugin {
373366
}
374367

375368
JSObject _getStaticPurchasesCommon() {
376-
final purchasesHybridMappings = globalContext['PurchasesHybridMappings'] as JSObject;
369+
final purchasesHybridMappings =
370+
globalContext['PurchasesHybridMappings'] as JSObject;
377371
JSObject? purchasesCommon;
378372
if (purchasesHybridMappings.has('PurchasesCommon')) {
379373
purchasesCommon = purchasesHybridMappings['PurchasesCommon'] as JSObject;
@@ -388,17 +382,16 @@ class PurchasesFlutterPlugin {
388382
}
389383

390384
Future _callStaticMethodReturningPromise(
391-
String methodName,
392-
List<dynamic> args,
393-
) async {
385+
String methodName,
386+
List<dynamic> args,
387+
) async {
394388
final promise = _callStaticMethod(methodName, args) as JSPromise;
395-
return promise.toDart
396-
.catchError((error) => throw _processError(error));
389+
return promise.toDart.catchError((error) => throw _processError(error));
397390
}
398391

399392
Future<Map<String, dynamic>> _getMapFromInstanceMethod(
400-
String methodName,
401-
List<dynamic> args,
393+
String methodName,
394+
List<dynamic> args,
402395
) async {
403396
final promise = _callInstanceMethod(methodName, args) as JSPromise;
404397

@@ -408,8 +401,8 @@ class PurchasesFlutterPlugin {
408401
}
409402

410403
Future<Map<String, dynamic>?> _getNullableMapFromInstanceMethod(
411-
String methodName,
412-
List<dynamic> args,
404+
String methodName,
405+
List<dynamic> args,
413406
) async {
414407
final promise = _callInstanceMethod(methodName, args) as JSPromise;
415408

@@ -423,12 +416,12 @@ class PurchasesFlutterPlugin {
423416
.catchError((error) => throw _processError(error));
424417
}
425418

426-
/// Calls a synchronous instance method on PurchasesCommon and converts
419+
/// Calls a synchronous instance method on PurchasesCommon and converts
427420
/// the result to a nullable Dart map.
428421
///
429422
/// Returns null if the JS method returns null, otherwise returns the converted map.
430423
Map<String, dynamic>? _getNullableMapFromInstanceSyncMethod(
431-
String methodName,
424+
String methodName,
432425
) {
433426
final result = _getInstance().callMethod(methodName.toJS);
434427
if (result == null) {
@@ -446,38 +439,40 @@ class PurchasesFlutterPlugin {
446439
final int arg => arg.toJS,
447440
final bool arg => arg.toJS,
448441
null => null,
449-
_ => throw ArgumentError(
450-
'Unsupported argument type: ${arg.runtimeType}',
451-
),
442+
_ =>
443+
throw ArgumentError('Unsupported argument type: ${arg.runtimeType}'),
452444
},
453445
];
454446

455447
PlatformException _processError(dynamic error) {
456-
if (error is JSObject && error.has('code')) {
457-
final errorMap = _convertJsRecordToMap(error);
458-
final code = errorMap['code'];
459-
final message = errorMap['message'];
460-
final underlyingErrorMessage = errorMap['underlyingErrorMessage'];
461-
final finalMessage = '$message. $underlyingErrorMessage';
462-
return PlatformException(
463-
code: '$code',
464-
message: finalMessage,
465-
details: errorMap,
466-
);
467-
} else {
468-
return PlatformException(
469-
code: _unknownErrorCode,
470-
message: error.toString(),
471-
);
448+
final jsAny = error as JSAny?;
449+
if (jsAny != null && jsAny.isA<JSObject>()) {
450+
final jsObject = jsAny as JSObject;
451+
if (jsObject.has('code')) {
452+
final errorMap = _convertJsRecordToMap(jsObject);
453+
final code = errorMap['code'];
454+
final message = errorMap['message'];
455+
final underlyingErrorMessage = errorMap['underlyingErrorMessage'];
456+
final finalMessage = '$message. $underlyingErrorMessage';
457+
return PlatformException(
458+
code: '$code',
459+
message: finalMessage,
460+
details: errorMap,
461+
);
462+
}
472463
}
464+
return PlatformException(
465+
code: _unknownErrorCode,
466+
message: error.toString(),
467+
);
473468
}
474469

475470
Map<String, dynamic> _convertJsRecordToMap(JSAny? jsRecord) {
476471
if (jsRecord == null) {
477472
throw ArgumentError('returned result cannot be null');
478473
} else {
479474
return Map<String, dynamic>.from(
480-
jsRecord.dartify() as Map<dynamic, dynamic>,
475+
jsRecord.dartify() as Map<dynamic, dynamic>,
481476
);
482477
}
483478
}

0 commit comments

Comments
 (0)