Table of contents
Links
I was scrolling through Reddit and came across a post titled: What could go wrong not paying your web developer. To summarize, it told the tale of a web developer who wasn't paid for their work on their client's website, so, he subsequently edited it to appear like this:
I thought this was hilarious!
So, I searched and found a myriad of other posts detailing similar things. Here are some other examples:
This made me want to build a tool that could be used to prevent cases such as these; at least, in relation to Flutter apps.
Below is a small glimpse into my brainstorming:
First, I ensured the name I wanted (killswitch
) was available on the Flutter/Dart package manager.
Second, I created a package in that name by running:
flutter create --template=package killswitch # <-- note the name
Next, inside /lib
, I created the following structure to keep the package clean:
/lib
βββ killswitch.dart # my export file
βββ src
βββ constants.dart # package-wide constants
βββ killed_page.dart # a page widget routed to upon the app being "killed"
βββ killswitch.dart # the widget containing all the killing/whitelisting logic
I then created a simple killed_page.dart
that looks like:
The page is not meant to be extravagant. It is simply intended to show a customizable message and then optionally provides a VoidCallback
upon the text being clicked (obviously with an animation, too) so that a developer can open a URL, copy their email address, etc. so the client can reach them.
Next, I created the Killswitch
class. As shown below, it has many fields for advanced customization:
class Killswitch extends StatefulWidget {
final String killedAppText;
final VoidCallback? killedAppTextClicked;
final VoidCallback? onKill;
final VoidCallback? onWhitelist;
final int killStatusCode;
final int whitelistStatusCode;
final int doNothingStatusCode;
final String killWhitelistAndIgnoreSourceUrl;
final Widget child;
final bool preventPushToKilledPage;
final String uniqueKillswitchWhitelistPrefsKey;
final String uniqueKillswitchWhitelistFailureConnectPrefsKey;
final int failuresToConnectToSourceBeforeWhitelist;
final bool suppressErrors;
...
However, because of its defaults, for common use cases, it can be used as simply as:
Killswitch(
killWhitelistAndIgnoreSourceUrl: "https://example.com", // <-- your URL
child: ...
);
The way it works is as follows:
On app open, asynchronously (so it doesn't slow down initialization), the Killswitch
polls your provided URL. If the URL returns a killStatusCode
status code, it routes the app to the killed page and triggers the onKill
callback. If the URL returns a whitelistStatusCode
status code, it locally saves this info to shared_preferences that the app is "whitelisted" and thus, it will never poll the URL again. Moreover, it triggers the onWhitelist
callback.
Moreover, in the case where the URL is polled n times (default = 10) and is able to connect to the URL but doesn't get either a killStatusCode
or whitelistStatusCode
response each time, it whitelists the app. Additionally, if there is a server error (httperror != http.ClientException
) n times, it also whitelists the app. This is to prevent extenuating circumstance. We don't want to think an error means the app should trigger the killswitch. We always err on the side of safety. If anything, an error should whitelist the app.
Finally, here is the killswitch in-use with a more advanced configuration (still a subset of all the possible options):
Killswitch(
killWhitelistAndIgnoreSourceUrl: "https://example.com/killswitch",
suppressErrors: false,
killedAppText: "Hi! This is the developer speaking. I killed the app. Please contact me for help by tapping this message.",
killedAppTextClicked: () => print("User tapped the killed app text"),
killStatusCode: 403,
whitelistStatusCode: 202,
onKill: () => print("App was killed"),
onWhitelist: () => print("App was whitelisted"),
uniqueKillswitchWhitelistFailureConnectPrefsKey: "THIS IS MY UNIQUE PREFS KEY BECAUSE I USE THE SAME KEY AS THEIRS ELSEWHERE IN MY APP",
failuresToConnectToSourceBeforeWhitelist: 25,
child: ...
);
It should be used high-up in your widget tree to be effective. Likely, this means directly under your MaterialApp
in your main.dart
's build method.
Let's say I build an app for a client and they promise to pay me when it's done. However, I've heard from others they are unreliable and tend to steal software. So, I add the Killswitch
widget to my app as a contingency and set its killWhitelistAndIgnoreSourceUrl
field to https://matthewtrent.me/killswitch (a domain in which I personally control unrelated to the client). As the forever-owner of this domain, I can make it return anything I want.
Now, upon the app opening, three things can happen after the killswitch checks this URL:
killStatusCode
is returned from my domain. This results in the app being killed (user brought to the killed_page.dart
screen). I will make my URL return this if the client refuses to pay me after I've already handed-over the app to them and am shut-out from editing it myself.whitelistStatusCode
is returned from my domain. This results in the app saving the fact it has been whitelisted and it'll never check if it should be killed again.doNothingStatusCode
is returned from my domain. This means "sit and wait; don't do anything now". I would likely return this while the app is in development.The integer values: killStatusCode
, whitelistStatusCode
, and doNothingStatusCode
are all configurable in the Killswitch
widget's code.
As a failsafe, upon the app trying to reach the specified URL n-times (ignoring cases where the client doesn't have connection), the app will automatically assume something went wrong with the domain configuration and be whitelisted, as to not accidentally kill an app.
First, I ensured my code was clean. This meant removing debugging messages, print statements, etc. I also was sure to use an my killswitch.dart
file (as showed in my file layout above) to only expose (export from /src
) what I wanted from my package's API.
Secondly, I commented my code using doc comments to achieve a high pub.dev comment score:
/// child widget of the killswitch (this is an example doc comment)
final Widget child;
Next, I added a LICENSE
file to my package. I elected to use the MIT license for my safety. It's also extremely popular for open-source software.
Then, I updated my CHANGELOG.md
to show the work I've done for each release. Also, I updated my README.md
to show an overview of what the package does.
Next, I ran flutter create example
inside my package's root directory to create an example of the package's usage. Flutter/Dart's package manager uses this; thus, it must be named exactly this. I proceeded to use the local version of my package inside this example app by adding this to my example app's pubspec.yaml
:
...
dependencies:
flutter:
sdk: flutter
killswitch: # <-- added this
path: ../ # <-- added this
...
Following this, at the top of my package's pubspec.yaml
, I added this:
name: killswitch
description: Remotely kill your application.
version: 0.0.1 # <-- this should match the version in your CHANGELOG.md
homepage: https://matthewtrent.me/
repository: https://github.com/mattrltrent/killswitch
issue_tracker: https://github.com/mattrltrent/killswitch/issues
The last bit of pre-publishing I had to do was run dart fix --dry-run
to see if the compiler could automatically detect if I needed to fix anything (usually formatting/style issues). I had no problems, though! However, if I had, I could always have ran dart fix
to fix them.
Finally, I ran dart pub publish --dry-run
to test publish my app and see if it looks good to the package manager. Considering no errors or fix-recommendations were shown, I ran dart pub publish
to publish it.
Then, a few minutes later, the package was up for public use here. Awesome!
This is not meant to be used nefariously. It is only meant to be used by two consenting parties and/or be for informative/testing purposes. DO NOT DO ANYTHING ILLEGAL. IT IS YOUR RESPONSBILITY TO ENSURE YOU'RE USING THIS CORRECTLY. Use at your own risk. This is not guaranteed to work perfectly, or as expected.