Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expo managed workflow - crash on web #917

Open
nzhenry opened this issue May 2, 2022 · 36 comments
Open

Expo managed workflow - crash on web #917

nzhenry opened this issue May 2, 2022 · 36 comments

Comments

@nzhenry
Copy link

nzhenry commented May 2, 2022

Describe the bug
Importing dependencies from @stripe/stripe-react-native causes app to crash on Expo managed workflow - web.

To Reproduce
Steps to reproduce the behavior:

  1. Create new expo managed workflow project
  2. expo install @stripe/stripe-react-native
  3. import stripe dependencies into component
  4. expo start --web
  5. See errors

Expected behavior
No errors unless stripe components are actually used at runtime.

Additional context
I'm aware that web isn't supported, but it would be nice if it would at least build so I could run my app. I don't plan to render any stripe components in the browser at runtime. Currently I can't figure out a way to make Webpack exclude this dependency. Any help would be greatly appreciated.

@charliecruzan-stripe
Copy link
Collaborator

charliecruzan-stripe commented May 4, 2022

See errors

What are the errors? Please share a github repo that we can use to test this

@nzhenry
Copy link
Author

nzhenry commented May 4, 2022

ERROR
08:03
./node_modules/react-native/Libraries/Components/TextInput/TextInputState.js:18:17
Module not found: Can't resolve '../../Utilities/Platform'
  16 | 
  17 | const React = require('react');
> 18 | const Platform = require('../../Utilities/Platform');
     |                 ^
  19 | const {findNodeHandle} = require('../../Renderer/shims/ReactNative');
  20 | import {Commands as AndroidTextInputCommands} from '../../Components/TextInput/AndroidTextInputNativeComponent';
  21 | import {Commands as iOSTextInputCommands} from '../../Components/TextInput/RCTSingelineTextInputNativeComponent';
ERROR
08:03
./node_modules/react-native/Libraries/Core/Timers/JSTimers.js:14:17
Module not found: Can't resolve '../../Utilities/Platform'
  12 | 
  13 | const BatchedBridge = require('../../BatchedBridge/BatchedBridge');
> 14 | const Platform = require('../../Utilities/Platform');
     |                 ^
  15 | const Systrace = require('../../Performance/Systrace');
  16 | 
  17 | const invariant = require('invariant');
ERROR
08:03
./node_modules/@stripe/stripe-react-native/lib/module/components/StripeProvider.js:1:515
Module not found: Can't resolve '../../package.json'
> 1 | var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(exports,"__esModule",{value:true});exports.StripeProvider=StripeProvider;exports.initStripe=void 0;var _regenerator=_interopRequireDefault(require("@babel/runtime/regenerator"));var _defineProperty2=_interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));var _react=require("react");var _NativeStripeSdk=_interopRequireDefault(require("../NativeStripeSdk"));var _helpers=require("../helpers");var _package=_interopRequireDefault(require("../../package.json"));function ownKeys(object,enumerableOnly){var keys=Object.keys(object);if(Object.getOwnPropertySymbols){var symbols=Object.getOwnPropertySymbols(object);if(enumerableOnly){symbols=symbols.filter(function(sym){return Object.getOwnPropertyDescriptor(object,sym).enumerable;});}keys.push.apply(keys,symbols);}return keys;}function _objectSpread(target){for(var i=1;i<arguments.length;i++){var source=arguments[i]!=null?arguments[i]:{};if(i%2){ownKeys(Object(source),true).forEach(function(key){(0,_defineProperty2.default)(target,key,source[key]);});}else if(Object.getOwnPropertyDescriptors){Object.defineProperties(target,Object.getOwnPropertyDescriptors(source));}else{ownKeys(Object(source)).forEach(function(key){Object.defineProperty(target,key,Object.getOwnPropertyDescriptor(source,key));});}}return target;}var EXPO_PARTNER_ID='pp_partner_JBN7LkABco2yUu';var repository=_package.default.repository;var appInfo={name:(0,_helpers.shouldAttributeExpo)()?_package.default.name+"/expo":_package.default.name,url:repository.url||repository,version:_package.default.version,partnerId:(0,_helpers.shouldAttributeExpo)()?EXPO_PARTNER_ID:undefined};var initStripe=function initStripe(params){var extendedParams;return _regenerator.default.async(function initStripe$(_context){while(1){switch(_context.prev=_context.next){case 0:extendedParams=_objectSpread(_objectSpread({},params),{},{appInfo:appInfo});_NativeStripeSdk.default.initialise(extendedParams);case 2:case"end":return _context.stop();}}},null,null,null,Promise);};exports.initStripe=initStripe;function StripeProvider(_ref){var children=_ref.children,publishableKey=_ref.publishableKey,merchantIdentifier=_ref.merchantIdentifier,threeDSecureParams=_ref.threeDSecureParams,stripeAccountId=_ref.stripeAccountId,urlScheme=_ref.urlScheme,setUrlSchemeOnAndroid=_ref.setUrlSchemeOnAndroid;(0,_react.useEffect)(function(){if(!publishableKey){return;}if(_helpers.isAndroid){_NativeStripeSdk.default.initialise({publishableKey:publishableKey,appInfo:appInfo,stripeAccountId:stripeAccountId,threeDSecureParams:threeDSecureParams,urlScheme:urlScheme,setUrlSchemeOnAndroid:setUrlSchemeOnAndroid});}else{_NativeStripeSdk.default.initialise({publishableKey:publishableKey,appInfo:appInfo,stripeAccountId:stripeAccountId,threeDSecureParams:threeDSecureParams,merchantIdentifier:merchantIdentifier,urlScheme:urlScheme});}},[publishableKey,merchantIdentifier,stripeAccountId,threeDSecureParams,urlScheme,setUrlSchemeOnAndroid]);return children;}
    |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   ^
  2 | //# sourceMappingURL=StripeProvider.js.map
ERROR
08:03
./node_modules/react-native/Libraries/Alert/Alert.js:13
Module not found: Can't resolve '../Utilities/Platform'
  11 | 'use strict';
  12 | 
> 13 | import Platform from '../Utilities/Platform';
  14 | import NativeDialogManagerAndroid, {
  15 |   type DialogOptions,
  16 | } from '../NativeModules/specs/NativeDialogManagerAndroid';
ERROR
08:03
./node_modules/react-native/Libraries/Core/NativeExceptionsManager.js:60:17
Module not found: Can't resolve '../Utilities/Platform'
  58 | }
  59 | 
> 60 | const Platform = require('../Utilities/Platform');
     |                 ^
  61 | 
  62 | const NativeModule = TurboModuleRegistry.getEnforcing<Spec>(
  63 |   'ExceptionsManager',
ERROR
08:03
./node_modules/react-native/Libraries/Core/setUpDeveloperTools.js:13
Module not found: Can't resolve '../Utilities/Platform'
  11 | 'use strict';
  12 | 
> 13 | import Platform from '../Utilities/Platform';
  14 | 
  15 | declare var console: typeof console & {_isPolyfilled: boolean, ...};
  16 | 
ERROR
08:03
./node_modules/react-native/Libraries/Core/ReactNativeVersionCheck.js:13
Module not found: Can't resolve '../Utilities/Platform'
  11 | 'use strict';
  12 | 
> 13 | import Platform from '../Utilities/Platform';
  14 | const ReactNativeVersion = require('./ReactNativeVersion');
  15 | 
  16 | /**
ERROR
08:03
./node_modules/react-native/Libraries/EventEmitter/NativeEventEmitter.js:13
Module not found: Can't resolve '../Utilities/Platform'
  11 | 'use strict';
  12 | 
> 13 | import Platform from '../Utilities/Platform';
  14 | import EventEmitter from '../vendor/emitter/EventEmitter';
  15 | import {type EventSubscription} from '../vendor/emitter/EventEmitter';
  16 | import RCTDeviceEventEmitter from './RCTDeviceEventEmitter';
ERROR
08:03
./node_modules/react-native/Libraries/Image/AssetSourceResolver.js:24:17
Module not found: Can't resolve '../Utilities/Platform'
  22 | 
  23 | const PixelRatio = require('../Utilities/PixelRatio');
> 24 | const Platform = require('../Utilities/Platform');
     |                 ^
  25 | 
  26 | const invariant = require('invariant');
  27 | 
ERROR
08:03
./node_modules/react-native/Libraries/ReactNative/PaperUIManager.js:14:17
Module not found: Can't resolve '../Utilities/Platform'
  12 | 
  13 | const NativeModules = require('../BatchedBridge/NativeModules');
> 14 | const Platform = require('../Utilities/Platform');
     |                 ^
  15 | const UIManagerProperties = require('./UIManagerProperties');
  16 | 
  17 | const defineLazyObjectProperty = require('../Utilities/defineLazyObjectProperty');
ERROR
08:03
./node_modules/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js:32:11
Module not found: Can't resolve '../Utilities/Platform'
  30 |   },
  31 |   get Platform(): Platform {
> 32 |     return require('../Utilities/Platform');
     |           ^
  33 |   },
  34 |   get RCTEventEmitter(): RCTEventEmitter {
  35 |     return require('../EventEmitter/RCTEventEmitter');
ERROR
08:03
./node_modules/react-native/Libraries/StyleSheet/processColor.js:13:17
Module not found: Can't resolve '../Utilities/Platform'
  11 | 'use strict';
  12 | 
> 13 | const Platform = require('../Utilities/Platform');
     |                 ^
  14 | 
  15 | const normalizeColor = require('./normalizeColor');
  16 | 
ERROR
08:03
./node_modules/react-native/Libraries/StyleSheet/processTransform.js:14:17
Module not found: Can't resolve '../Utilities/Platform'
  12 | 
  13 | const MatrixMath = require('../Utilities/MatrixMath');
> 14 | const Platform = require('../Utilities/Platform');
     |                 ^
  15 | 
  16 | const invariant = require('invariant');
  17 | const stringifySafe = require('../Utilities/stringifySafe').default;
ERROR
08:03
./node_modules/react-native/Libraries/Utilities/HMRClient.js:16:17
Module not found: Can't resolve './Platform'
  14 | const invariant = require('invariant');
  15 | const MetroHMRClient = require('metro-runtime/src/modules/HMRClient');
> 16 | const Platform = require('./Platform');
     |                 ^
  17 | const prettyFormat = require('pretty-format');
  18 | 
  19 | import getDevServer from '../Core/Devtools/getDevServer';
ERROR
08:03
./node_modules/react-native/Libraries/StyleSheet/processColor.js:34:31
Module not found: Can't resolve './PlatformColorValueTypes'
  32 | 
  33 |   if (typeof normalizedColor === 'object') {
> 34 |     const processColorObject = require('./PlatformColorValueTypes')
     |                               ^
  35 |       .processColorObject;
  36 | 
  37 |     const processedColorObj = processColorObject(normalizedColor);
ERROR
08:03
./node_modules/react-native/Libraries/StyleSheet/normalizeColor.js:24:35
Module not found: Can't resolve './PlatformColorValueTypes'
  22 | ): ?ProcessedColorValue {
  23 |   if (typeof color === 'object' && color != null) {
> 24 |     const {normalizeColorObject} = require('./PlatformColorValueTypes');
     |                                   ^
  25 |     const normalizedColor = normalizeColorObject(color);
  26 |     if (normalizedColor != null) {
  27 |       return color;
ERROR
08:03
./node_modules/react-native/Libraries/Alert/Alert.js:17
Module not found: Can't resolve './RCTAlertManager'
  15 |   type DialogOptions,
  16 | } from '../NativeModules/specs/NativeDialogManagerAndroid';
> 17 | import RCTAlertManager from './RCTAlertManager';
  18 | 
  19 | export type AlertType =
  20 |   | 'default'
ERROR
08:03
./node_modules/react-native/Libraries/Network/XMLHttpRequest.js:18:22
Module not found: Can't resolve './RCTNetworking'
  16 | const EventTarget = require('event-target-shim');
  17 | const GlobalPerformanceLogger = require('../Utilities/GlobalPerformanceLogger');
> 18 | const RCTNetworking = require('./RCTNetworking');
     |                      ^
  19 | 
  20 | const base64 = require('base64-js');
  21 | const invariant = require('invariant');

@nzhenry
Copy link
Author

nzhenry commented May 4, 2022

Here's a sample repo https://github.com/nzhenry/stripe-expo-web-sample

@charliecruzan-stripe
Copy link
Collaborator

There seem to be other errors besides stripe-react-native, are those expected for you?

@nzhenry
Copy link
Author

nzhenry commented May 4, 2022

No. All the errors were introduced when I added the Stripe library.

@charliecruzan-stripe
Copy link
Collaborator

Gotchya, we probably just need to stub out almost-empty .web files

@olaurendeau
Copy link

I've just ran into the same issue, would be nice to see those stubbing web files added :)

@nzhenry
Copy link
Author

nzhenry commented May 19, 2022

@olaurendeau I have a workaround for now. I've implemented separate application entry points for mobile and web, with a basic dependency injection container to abstract away the platform dependent components. It's not ideal, but it works.

@anabeatrizzz
Copy link

anabeatrizzz commented Jun 14, 2022

I'm facing only this error:

Module not found: Can't resolve './RCTNetworking'
  16 | const EventTarget = require('event-target-shim');
  17 | const GlobalPerformanceLogger = require('../Utilities/GlobalPerformanceLogger');
> 18 | const RCTNetworking = require('./RCTNetworking');
     |                      ^
  19 |
  20 | const base64 = require('base64-js');
  21 | const invariant = require('invariant');

@ebo7
Copy link

ebo7 commented Sep 28, 2022

@olaurendeau, hello thank you for the suggestion! If you have time, can you explain more about the dependency injection container to abstract away the platform dependent components ?

@FelixChong980212
Copy link

#917 (comment)

Hi Any Ideas to solve the question? I facing this problem too

@robert-nash
Copy link

I am still working through the first web build of my app but I seem to have got round this particular error for the meantime by adding null-loader as an alias for @stripe/stripe-react-native in my webpack config.

I added a webpack.config.js as described here and then modified it so it looks like this excerpt. I also installed the null-loader package.

webpack.config.js

const createExpoWebpackConfigAsync = require('@expo/webpack-config');

module.exports = async function (env, argv) {
  const config = await createExpoWebpackConfigAsync(env, argv);
  config.resolve.alias["@stripe/stripe-react-native"] = "null-loader";
  return config;
};

@MehmetKaplan
Copy link

MehmetKaplan commented Feb 20, 2023

Let me make the steps explicit for the lazy folks like future me. (All credits to @robert-nash , in his previous answer.)

  1. Add null-loader

    yarn add -D null-loader
  2. Generate the webpack.config.js file with expo's template:

    npx expo customize webpack.config.js
  3. Modify webpack.config.js, add the line marked with the comment ADD THIS LINE. Final version of the file should look like:

    // webpack.config.js
    const createExpoWebpackConfigAsync = require('@expo/webpack-config');
    
    module.exports = async function (env, argv) {
    	const config = await createExpoWebpackConfigAsync(env, argv);
    	// Customize the config before returning it.
    	config.resolve.alias["@stripe/stripe-react-native"] = "null-loader"; // ADD THIS LINE
    	return config;
    };

@RobertMercieca
Copy link

Facing this issue too, the fix posted by @robert-nash / @MehmetKaplan is not working for me.

@JorensM
Copy link

JorensM commented Sep 1, 2023

I'm also having this issue.

@gunwant11
Copy link

facing same error

@markedwards
Copy link

For me this was caused by lottie-react-native.

@malusiTowo
Copy link

facing same issue.The fixes above are not working for me. Anybody else have any ideas how to resolve this issue?

@kynectionjerry
Copy link

I'm facing only this error:

Module not found: Can't resolve './RCTNetworking'
  16 | const EventTarget = require('event-target-shim');
  17 | const GlobalPerformanceLogger = require('../Utilities/GlobalPerformanceLogger');
> 18 | const RCTNetworking = require('./RCTNetworking');
     |                      ^
  19 |
  20 | const base64 = require('base64-js');
  21 | const invariant = require('invariant');

Did you fix it mate ?

@kynectionjerry
Copy link

I'm facing only this error:

Module not found: Can't resolve './RCTNetworking'
  16 | const EventTarget = require('event-target-shim');
  17 | const GlobalPerformanceLogger = require('../Utilities/GlobalPerformanceLogger');
> 18 | const RCTNetworking = require('./RCTNetworking');
     |                      ^
  19 |
  20 | const base64 = require('base64-js');
  21 | const invariant = require('invariant');

Did you fix it mate ?

@anabeatrizzz

@mubeen1519
Copy link

mubeen1519 commented Nov 27, 2023

hey guyzz for the stripe issue you can initially add null loader like this to resolve the TextInputState Error
here is my webpackConfig.js file for adding alias
Screenshot from 2023-11-27 17-36-09
and create a separate NullModuleloader file like this
Screenshot from 2023-11-27 17-37-11

@JorensM
Copy link

JorensM commented Jan 17, 2024

I managed to solve this by using Platform extensions. I created a file stripe.ts for web which exports placeholder components/functions, and created a file stripe.native.ts for mobile which exports the actual stripe components/functions. Now I can import by ./stripe and it will automatically import the correct module based on which platform I'm using.

@heinrichcoetzee
Copy link

This is seriously still an issue...

@paulmbw
Copy link

paulmbw commented Jun 2, 2024

I've asked the devs who maintain expo as well for guidance on this as I'm also running into this issue https://x.com/paulwaweruco/status/1797291557617340591

I managed to solve this by using Platform extensions. I created a file stripe.ts for web which exports placeholder components/functions, and created a file stripe.native.ts for mobile which exports the actual stripe components/functions. Now I can import by ./stripe and it will automatically import the correct module based on which platform I'm using.

@JorensM I've tried your solution. I've got a stripe.native.tsx file which exports the actual stripe components/functions, and I've got a stripe.web.tsx with placeholders.

I then import both files in a StripeProvider and decide which file to render based on the Platform:

import { Platform } from "react-native";
import { StripeProvider as WebStripeProvider } from "./stripe.web";
import { StripeProvider as NativeStripeProvider } from "./stripe.native";

type StripeProviderProps = {
  children: JSX.Element | JSX.Element[];
};

const StripeProvider: React.FC<StripeProviderProps> = ({
  children,
}: StripeProviderProps): JSX.Element => {
  const publishableKey = process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY;

  if (!publishableKey) {
    throw new Error(
      "publishableKey is not set. Ensure that EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY is set in your environment variables."
    );
  }

  if (Platform.OS === "web") {
    return (
      <WebStripeProvider publishableKey={publishableKey}>
        {children}
      </WebStripeProvider>
    );
  }

  return (
    <NativeStripeProvider publishableKey={publishableKey}>
      {children}
    </NativeStripeProvider>
  );
};

export default StripeProvider;

I'm still running into the same error:

Unable to resolve module ../../Utilities/Platform from /Users/paulwaweru/Projects/launchtodayhq/launchtoday/node_modules/react-native/Libraries/Components/TextInput/TextInputState.js

Is there something we might be missing?

@heinrichcoetzee
Copy link

I managed to solve this by using Platform extensions. I created a file stripe.ts for web which exports placeholder components/functions, and created a file stripe.native.ts for mobile which exports the actual stripe components/functions. Now I can import by ./stripe and it will automatically import the correct module based on which platform I'm using.

Any chance you can share what the stripe.ts and stripe.native.ts looks like and how its used when importing?

@MehmetKaplan
Copy link

A workaround

(This is not the exact solution you are looking for.)

I made a library that unifies web's and react-native's web-views. Using it, you can use the web version. The library is react-native-webview-with-web, (naming is hard).

And using this I made another library, tamed-stripe which covers end to end whole structure (including a backend that answers the webhooks also). If specs are suitable (Postgresql, node.js backend, etc), it covers mostly used functions and as stated above paragraph it uses the web version of Stripe screens. It also has an example application with node.js backend and Expo frontend here.

@JorensM
Copy link

JorensM commented Jun 9, 2024

@paulmbw
@heinrichcoetzee

Sorry for the late response. When you make stripe.native.js and stripe.js, you just import /stripe and it will import the correct module depending on the environment where the code is run (web or mobile)

For clarity, here are my files:

stripe.tsx

// DO NOT IMPORT THIS FILE, import the index.ts instead

// Since stripe-react-native is not available on web, this just has non-functioning
// placeholders

import { PropsWithChildren } from 'react'

type StripeProviderProps = {
    publishableKey: string
}

export const StripeProvider = ({ publishableKey, children }: PropsWithChildren<StripeProviderProps>) => {
    return (
        <>
         {children}
        </>
    )
}

type StripeHook = (() => null) |
(() => {
    initPaymentSheet: (context: any) => { error: any }
    presentPaymentSheet: () => { error: any }
})

export const useStripe: StripeHook = () => null;

stripe.native.tsx

// DO NOT IMPORT THIS FILE, import the index.ts instead

// Core
import { StripeProvider, useStripe } from '@stripe/stripe-react-native';

export { StripeProvider, useStripe }

index.ts

import { StripeProvider, useStripe } from './stripe';

export { StripeProvider, useStripe }

Then you just import the index.ts file and the appropriate stripe file will be loaded depending on your environment. I think the reason I had an index.ts file was just so I could import from the folder instead of file, like /stripe instead of /stripe/stripe, but if you don't have a separate stripe folder then I think you can skip the index.ts file and just import /stripe.

Sorry if this is confusing, if so let me know and I'll try to clarify.

@paulmbw
Copy link

paulmbw commented Jun 13, 2024

Awesome, thank you @JorensM !

I've written up a blog post to expand on this further, let me know if you have any questions

https://blog.launchtoday.dev/article/integrate-stripe-expo

@Abdullah4Jovera
Copy link

Hi @paulmbw please upload the project that you have create in the above blog......

@paulmbw
Copy link

paulmbw commented Jun 14, 2024

Hi @Abdullah4Jovera

You should have been able to access the project linked in the blog

Xnapper-2024-06-14-12 39 14

@Abdullah4Jovera
Copy link

Thanks alot @paulmbw

@moza88
Copy link

moza88 commented Jul 8, 2024

@olaurendeau I have a workaround for now. I've implemented separate application entry points for mobile and web, with a basic dependency injection container to abstract away the platform dependent components. It's not ideal, but it works.

How do you do this? I am facing the same issue, it's weird these issues were reported back in 2022 and in 2024 the same issues persist.

@errantSquam
Copy link

errantSquam commented Oct 20, 2024

New solution works, but lazy TL;DR for anyone facing the problem on Expo Web (since webpack doesn't seem to be supported anymore):

  1. Find your .js or .tsx files with Stripe
  2. Make a .web.js or .web.tsx copy of those files
  3. Remove Stripe functions, then add in dummied out functions in their place

Preferably for a larger project, you might want to abstract out your Stripe functions instead, then make components that return the exact functionality (on mobile) or a dummied out function( on web), instead of maintaining two identical versions of your pages.

Relevant documentation on platform specific extensions here: https://docs.expo.dev/router/advanced/platform-specific-modules/#platform-specific-extensions

@tim5go
Copy link

tim5go commented Dec 14, 2024

Thanks @JorensM
I've verified your solution, and it does work !
Also thanks @paulmbw for writing a blog for further explanation !

@ciccilleju
Copy link

Awesome, thank you @JorensM !

I've written up a blog post to expand on this further, let me know if you have any questions

https://blog.launchtoday.dev/article/integrate-stripe-expo

hello! so, if i've understood correctly, we need 2 separate libraries, stripe-react-native for ios and android and stripe-js for web?
is it correct?

thanks

@MehmetKaplan
Copy link

Awesome, thank you @JorensM !
I've written up a blog post to expand on this further, let me know if you have any questions
https://blog.launchtoday.dev/article/integrate-stripe-expo

hello! so, if i've understood correctly, we need 2 separate libraries, stripe-react-native for ios and android and stripe-js for web? is it correct?

thanks

I resolved this issue with the workaround I mentioned in my post above.

The library contains everything in an opinionated way.

  1. Backend: PostgreSQL, node.js
  2. Frontend (iOS, Android, web): Expo
  3. Uses a library called react-native-webview-with-web that combines the react native webview and browser webview.
  4. Using the combined WebView mentioned in (3) utilizes the check out sessions that Stripe provides.

An end to end example app is here.

Disclaimer: I implemented the mentioned libraries but the post is not to advertise them. Use them if and only if they address your concerns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests