Compare commits

..

No commits in common. "master" and "2.0.3" have entirely different histories.

18 changed files with 198 additions and 606 deletions

View file

@ -1,14 +0,0 @@
name: Iconica Standard Component Release Workflow
# Workflow Caller version: 1.0.0
on:
release:
types: [published]
workflow_dispatch:
jobs:
call-global-iconica-workflow:
uses: Iconica-Development/.github/.github/workflows/component-release.yml@master
secrets: inherit
permissions: write-all

4
.gitignore vendored
View file

@ -44,7 +44,3 @@ app.*.map.json
ios ios
.metadata .metadata
pubspec.lock pubspec.lock
# FVM Version Cache
.fvm/
.fvmrc

View file

@ -1,46 +1,3 @@
## 4.2.4
- Fixed the userstory to always call the splashScreenFuture and killswitchservice logic when a custom splashScreenBuilder is provided
## 4.2.3
- Added check if introduction should be shown according to the service before showing the introduction at all
## 4.2.2
- Added custom navigator in the root of the navigator user-story
## 4.2.1
- Updated flutter_introduction to 5.0.0
## 4.1.0
- Updated README
- Removed check if the introductions should be shown.
- Updated flutter_introduction to 3.1.0
## 4.0.0
- Added default introduction page.
- Added default splash screen.
- Changed the way the splash screen is enabled/disabled.
## 3.0.0
BREAKING:
- add NavigatorStartUserStory widget
- change homeEntry to an onComplete callback
Other changes:
- add parameter to configuration to supply an implementation of the killswitch service
- call splash handler when no builder is provided
- add mounted check to navigation after async gap for navigator version
- rename myFunction in splashscreen function to splashHandler
- add return after routing on navigator version
## 2.0.5
- Added canPopFromIntroduction to enable/disable popping from introduction screens
## 2.0.4
- Removed `AlwaysShowIntroduction` option, changed naming of `isKillSwitchActive` to `isAllowedToPassThrough`.
## 2.0.3 ## 2.0.3
- Added after splashscreen route - Added after splashscreen route

View file

@ -1,194 +0,0 @@
# Contributing
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued.
See the [Table of Contents](#table-of-contents) for different ways to help and details about how we handle them.
Please make sure to read the relevant section before making your contribution.
It will make it a lot easier for us maintainers and smooth out the experience for all involved.
Iconica looks forward to your contributions. 🎉
## Table of contents
- [Code of conduct](#code-of-conduct)
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Contributing code](#contributing-code)
## Code of conduct
### Legal notice
When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
All accepted pull requests and other additions to this project will be considered intellectual property of Iconica.
All repositories should be kept clean of jokes, easter eggs and other unnecessary additions.
## I have a question
If you want to ask a question, we assume that you have read the available documentation found within the code.
Before you ask a question, it is best to search for existing issues that might help you.
In case you have found a suitable issue but still need clarification, you can ask your question
It is also advisable to search the internet for answers first.
If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open an issue.
- Provide as much context as you can about what you're running into.
We will then take care of the issue as soon as possible.
## I want to contribute
### Reporting bugs
<!-- omit in toc -->
**Before submitting a bug report**
A good bug report shouldn't leave others needing to chase you up for more information.
Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report.
Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error.
- Also make sure to search the internet (including Stack Overflow) to see if users outside of Iconica have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Time and date of occurance
- Describe the expected result and actual result
- Can you reliably reproduce the issue? And can you also reproduce it with older versions? Describe all steps that lead to the bug.
Once it's filed:
- The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps.
If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for additional information.
- If the team is able to reproduce the issue, it will be moved into the backlog, as well as marked with a priority, and the issue will be left to be [implemented by someone](#contributing-code).
### Contributing code
When you start working on your contribution, make sure you are aware of the relevant documentation and the functionality of the component you are working on.
When writing code, follow the style guidelines set by Dart: [Effective Dart](https://Dart.dev/guides/language/effective-Dart). This contains most information you will need to write clean Dart code that is well documented.
**Documentation**
As Effective Dart indicates, documenting your public methods with Dart doc comments is recommended.
Aside from Effective Dart, we require specific information in the documentation of a method:
At the very least, your documentation should first name what the code does, then followed below by requirements for calling the method, the result of the method.
Any references to internal variables or other methods should be done through [var] to indicate a reference.
If the method or class is complex enough (determined by the reviewers) an example is required.
If unsure, add an example in the docs using code blocks.
For classes and methods, document the individual parameters with their implications.
> Tip: Remember that the shortest documentation can be written by having good descriptive names in the first place.
An example:
```Dart
library iconica_utilities.bidirectional_sorter;
part 'sorter.Dart';
part 'enum.Dart';
/// Generic sort method, allow sorting of list with primitives or complex types.
/// Uses [SortDirection] to determine the direction, either Ascending or Descending,
/// Gets called on [List] toSort of type [T] which cannot be shorter than 2.
/// Optionally for complex types a [Comparable] [Function] can be given to compare complex types.
/// ```
/// List<TestObject> objects = [];
/// for (int i = 0; i < 10; i++) {
/// objects.add(TestObject(name: "name", id: i));
/// }
///
/// sort<TestObject>(
/// SortDirection.descending, objects, (object) => object.id);
///
/// ```
/// In the above example a list of TestObjects is created, and then sorted in descending order.
/// If the implementation of TestObject is as following:
/// ```
/// class TestObject {
/// final String name;
/// final int id;
///
/// TestObject({required this.name, required this.id});
/// }
/// ```
/// And the list is logged to the console, the following will appear:
/// ```
/// [name9, name8, name7, name6, name5, name4, name3, name2, name1, name0]
/// ```
void sort<T>(
/// Determines the sorting direction, can be either Ascending or Descending
SortDirection sortDirection,
/// Incoming list, which gets sorted
List<T> toSort, [
/// Optional comparable, which is only necessary for complex types
SortFieldGetter<T>? sortValueCallback,
]) {
if (toSort.length < 2) return;
assert(
toSort.whereType<Comparable>().isNotEmpty || sortValueCallback != null);
BidirectionalSorter<T>(
sortInstructions: <SortInstruction<T>>[
SortInstruction(
sortValueCallback ?? (t) => t as Comparable, sortDirection),
],
).sort(toSort);
}
/// same functionality as [sort] but with the added functionality
/// of sorting multiple values
void sortMulti<T>(
/// Incoming list, which gets sorted
List<T> toSort,
/// list of comparables to sort multiple values at once,
/// priority based on index
List<SortInstruction<T>> sortValueCallbacks,
) {
if (toSort.length < 2) return;
assert(sortValueCallbacks.isNotEmpty);
BidirectionalSorter<T>(
sortInstructions: sortValueCallbacks,
).sort(toSort);
}
```
**Tests**
For each public method that was created, excluding widgets, which contains any form of logic (e.g. Calculations, predicates or major side-effects) tests are required.
A set of tests is written for each method, covering at least each path within the method. For example:
```Dart
void foo() {
try {
var bar = doSomething();
if (bar) {
doSomethingElse();
} else {
doSomethingCool();
}
} catch (_) {
displayError();
}
}
```
The method above should result in 3 tests:
1. A test for the path leading to displayError by the cause of an exception
2. A test for if bar is true, resulting in doSomethingElse()
3. A test for if bar is false, resulting in the doSomethingCool() method being called.
This means that we require 100% coverage of each method you test.

View file

@ -1,9 +0,0 @@
Copyright (c) 2024 Iconica, All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

108
README.md
View file

@ -6,62 +6,57 @@ Flutter_start is a package that allows you to jumpstart your application with a
To use this package, add flutter_start as a dependency in your pubspec.yaml file: To use this package, add flutter_start as a dependency in your pubspec.yaml file:
```yaml ```
flutter_start: flutter_start:
git: git:
url: https://github.com/Iconica-Development/flutter_start url: https://github.com/Iconica-Development/flutter_start
ref: 4.1.0 ref: <Version>
``` ```
## go_router To use the module within your Flutter-application with predefined `Go_router` routes you should add the following:
Add `go_router` as dependency to your project.
Add go_router as dependency to your project.
Add the following configuration to your flutter_application: Add the following configuration to your flutter_application:
``` ```
List<GoRoute> getStartStoryRoutes() => getStartStoryRoutes(); StartUserStoryConfiguration startUserStoryConfiguration = const StartUserStoryConfiguration();
``` ```
Or with options: and set the values as you wish.
Place the following code somewhere in your project where it can be accessed globally: Next add the `StartUserStoryConfiguration` to `getStartStoryRoutes` Like so:
``` ```
var startUserStoryConfiguration = const StartUserStoryConfiguration(); List<GoRoute> getStartRoutes() => getStartStoryRoutes(
``` startUserStoryConfiguration,
```
List<GoRoute> getStartStoryRoutes() => getStartStoryRoutes(
configuration: startUserStoryConfiguration,
); );
``` ```
Finally add the `getStartRoutes` to your `go_router` routes like so: Finally add the `getStartRoutes` to your `Go_router` routes like so:
``` ```
final GoRouter _router = GoRouter( final GoRouter _router = GoRouter(
routes: <RouteBase>[ routes: <RouteBase>[
...getStartStoryRoutes() ...getStartRoutes()
], ],
); );
``` ```
The routes that can be used to navigate are: The routes that can be used to navigate are:
For routing to the `splashScreen`: For routing to the `SplashScreen`:
``` ```
static const String splashScreen = '/splashScreen'; static const String splashScreen = '/splashScreen';
``` ```
For routing to the `introduction`: For routing to the `Introduction`:
``` ```
static const String introduction = '/introduction'; static const String introduction = '/introduction';
``` ```
For routing to the `home`: For routing to the `HomeEntry`:
``` ```
static const String home = '/home'; static const String home = '/home';
@ -78,73 +73,30 @@ final GoRouter _router = GoRouter(
); );
``` ```
## Navigator To use the module within your Flutter-application without predefined `Go_router` routes but with `Navigator` routes add the following :
Add the following code to the build-method of a chosen widget like so: Add the following configuration to your flutter_application:
```
class Example extends StatelessWidget {
const Example({super.key});
@override
Widget build(BuildContext context) {
return NavigatorStartUserStory(
onComplete: (context) {},
);
}
}
```
or with options:
Place the following code somewhere in your project where it can be accessed globally:
``` ```
var startUserStoryConfiguration = const StartUserStoryConfiguration(); StartUserStoryConfiguration startUserStoryConfiguration = const StartUserStoryConfiguration();
``` ```
``` Add the following code to the build-method of a chosen widget:
class Example extends StatelessWidget {
const Example({super.key});
@override
Widget build(BuildContext context) {
return NavigatorStartUserStory(
configuration: startUserStoryConfiguration,
onComplete: (context) {},
);
}
}
``` ```
startNavigatorUserStory(startUserStoryConfiguration, context);
```
If the splashScreenBuilder is not used the SplashScreen will be skipped.
The `StartUserStoryConfiguration` has its own parameters, as specified below: The `StartUserStoryConfiguration` has its own parameters, as specified below:
| Parameter | Explanation | | Parameter | Explanation |
|-----------|-------------| |-----------|-------------|
| `splashScreenBuilder` | The builder to override the default splashScreen | | splashScreenBuilder | The builder for the splashScreen. |
| `introductionBuilder` | A builder to wrap the introductions in your own page | | introductionOptions | The options for the introduction. |
| `introductionOptionsBuilder` | The builder to override the default `introductionOptions` | | introductionService | The service for the introduction. Default IntroductionService (SharedPreferencesIntroductionDataProvider()) |
|`introductionService` | The service to override the default `introductionService` | | homeEntry | The widget that will be shown after the introduction. |
| `homeScreenRoute` | The route to navigate to after the introduction or splashScreen is completed | | introductionFallbackScreen | The widget that will be shown when the introduction is skipped. |
| `introductionFallbackScreen` | The screen that is shown when something goes wrong fetching data for the introduction | | introductionScrollPhysics | The scrollPhysics for the introduction. |
| `introductionScrollPhysics` | The scroll physics for the introduction | | showIntroduction | Whether or not the introduction should be shown. |
| `showIntroduction` | A boolean to show the introduction or not. Defaults to true | | useKillswitch | Whether or not the killswitch should be used. This will only work when you use the splashScreen and you need to have a active internet connection|
| `useKillswitch` | A boolean to use the killswitch or not. Defaults to false |
| `minimumSplashScreenDuration` | The minimum duration the splashScreen should be shown. Defaults to 3 seconds |
| `splashScreenFuture` | The future to be completed before the splashScreen is completed |
| `splashScreenCenterWidget` | The widget to be shown in the center of the splashScreen |
| `splashScreenBackgroundColor` | The color of the splashScreen background. Defaults to Color(0xff212121) |
| `canPopFromIntroduction` | Allow popping from introduction, defaults to true. Defaults to true |
| `killswitchService` | The service to override the default killswitch service |
| `showSplashScreen` | A boolean to show the splashScreen or not. Defaults to true |
## Issues
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_start) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
## Want to contribute
[text](about:blank#blocked)
If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_start/pulls).
## Author
This flutter_start for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>

3
example/.gitignore vendored
View file

@ -31,9 +31,6 @@ migrate_working_dir/
.pub/ .pub/
/build/ /build/
# platforms
/web
# Symbolication related # Symbolication related
app.*.symbols app.*.symbols

View file

@ -1,16 +0,0 @@
# example
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View file

@ -20,18 +20,12 @@ class Home extends StatelessWidget {
const Home({super.key}); const Home({super.key});
@override @override
Widget build(BuildContext context) => NavigatorStartUserStory( Widget build(BuildContext context) =>
configuration: config, startNavigatorUserStory(config, context);
onComplete: (context) async {
await Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const HomeEntry()),
);
},
);
} }
List<GoRoute> getStartRoutes() => getStartStoryRoutes( List<GoRoute> getStartRoutes() => getStartStoryRoutes(
configuration: config, config,
); );
StartUserStoryConfiguration config = StartUserStoryConfiguration( StartUserStoryConfiguration config = StartUserStoryConfiguration(
@ -39,6 +33,39 @@ StartUserStoryConfiguration config = StartUserStoryConfiguration(
splashScreenBuilder: (context, onFinish) => SplashScreen( splashScreenBuilder: (context, onFinish) => SplashScreen(
onFinish: onFinish, onFinish: onFinish,
), ),
homeEntry: const HomeEntry(),
introductionOptionsBuilder: (ctx) => IntroductionOptions(
pages: [
IntroductionPage(
title: const Text('First page'),
text: const Text('Wow a page'),
graphic: const FlutterLogo(),
),
IntroductionPage(
title: const Text('Second page'),
text: const Text('Another page'),
graphic: const FlutterLogo(),
),
IntroductionPage(
title: const Text('Third page'),
text: const Text('The final page of this app'),
graphic: const FlutterLogo(),
),
],
introductionTranslations: const IntroductionTranslations(
skipButton: 'Skip it!',
nextButton: 'Next',
previousButton: 'Previous',
finishButton: 'To the app!',
),
tapEnabled: true,
displayMode: IntroductionDisplayMode.multiPageHorizontal,
buttonMode: IntroductionScreenButtonMode.text,
indicatorMode: IndicatorMode.dash,
skippable: true,
buttonBuilder: (context, onPressed, child, type) =>
ElevatedButton(onPressed: onPressed, child: child),
),
); );
class SplashScreen extends StatefulWidget { class SplashScreen extends StatefulWidget {
@ -82,3 +109,17 @@ class HomeEntry extends StatelessWidget {
body: const Center(child: Text('HomeEntry')), body: const Center(child: Text('HomeEntry')),
); );
} }
class ExampleIntroductionDataProvider
implements SharedPreferencesIntroductionDataProvider {
@override
String key = 'example';
@override
Future<void> setCompleted({bool value = true}) async =>
// ignore: void_checks
false;
@override
Future<bool> shouldShow() async => true;
}

View file

@ -12,8 +12,10 @@ dependencies:
flutter_start: flutter_start:
path: ../ path: ../
flutter_introduction_shared_preferences: flutter_introduction_shared_preferences:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ git:
version: ^5.0.0 url: https://github.com/Iconica-Development/flutter_introduction
ref: 2.1.0
path: packages/flutter_introduction_shared_preferences
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -5,14 +5,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
/// Builds a screen with a fade transition.
///
/// The [context] parameter is the [BuildContext] in which this widget is built.
/// The [state] parameter is the [GoRouterState] that contains
/// the current routing state.
/// The [child] parameter is the widget that will be displayed on the screen.
///
/// Returns a [CustomTransitionPage] with a fade transition.
CustomTransitionPage buildScreenWithFadeTransition<T>({ CustomTransitionPage buildScreenWithFadeTransition<T>({
required BuildContext context, required BuildContext context,
required GoRouterState state, required GoRouterState state,
@ -25,14 +17,6 @@ CustomTransitionPage buildScreenWithFadeTransition<T>({
FadeTransition(opacity: animation, child: child), FadeTransition(opacity: animation, child: child),
); );
/// Builds a screen without any transition.
///
/// The [context] parameter is the [BuildContext] in which this widget is built.
/// The [state] parameter is the [GoRouterState] that contains
/// the current routing state.
/// The [child] parameter is the widget that will be displayed on the screen.
///
/// Returns a [CustomTransitionPage] without any transition.
CustomTransitionPage buildScreenWithoutTransition<T>({ CustomTransitionPage buildScreenWithoutTransition<T>({
required BuildContext context, required BuildContext context,
required GoRouterState state, required GoRouterState state,

View file

@ -1,29 +1,24 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_introduction/flutter_introduction.dart'; import 'package:flutter_introduction/flutter_introduction.dart';
import 'package:flutter_start/src/services/killswitch_service.dart';
/// An immutable class that represents the configuration for
/// starting a user story.
@immutable @immutable
class StartUserStoryConfiguration { class StartUserStoryConfiguration {
/// Creates a new instance of [StartUserStoryConfiguration].
const StartUserStoryConfiguration({ const StartUserStoryConfiguration({
this.splashScreenBuilder, this.splashScreenBuilder,
this.introductionBuilder, this.introductionBuilder,
this.introductionOptionsBuilder, this.introductionOptionsBuilder,
this.introductionService, this.introductionService,
this.homeEntry,
this.homeScreenRoute, this.homeScreenRoute,
this.introductionFallbackScreen, this.introductionFallbackScreen,
this.introductionScrollPhysics, this.introductionScrollPhysics,
this.showIntroduction = true, this.showIntroduction = true,
this.alwaysShowIntroduction = false,
this.useKillswitch = false, this.useKillswitch = false,
this.minimumSplashScreenDuration = 3, this.minimumSplashScreenDuration = 3,
this.splashScreenFuture, this.splashScreenFuture,
this.splashScreenCenterWidget, this.splashScreenCenterWidget,
this.splashScreenBackgroundColor = const Color(0xff212121), this.splashScreenBackgroundColor,
this.canPopFromIntroduction = true,
this.killswitchService,
this.showSplashScreen = true,
}); });
/// You can use this to build your own splash screen. /// You can use this to build your own splash screen.
@ -41,16 +36,20 @@ class StartUserStoryConfiguration {
/// The route that is used to navigate to the home screen. /// The route that is used to navigate to the home screen.
final String? homeScreenRoute; final String? homeScreenRoute;
final Widget? homeEntry;
final IntroductionOptions Function(BuildContext context)? final IntroductionOptions Function(BuildContext context)?
introductionOptionsBuilder; introductionOptionsBuilder;
final Widget? introductionFallbackScreen; final Widget? introductionFallbackScreen;
final IntroductionService? introductionService; final IntroductionService? introductionService;
final KillswitchService? killswitchService;
final ScrollPhysics? introductionScrollPhysics; final ScrollPhysics? introductionScrollPhysics;
/// If the introduction should be shown. /// If the introduction should be shown.
final bool showIntroduction; final bool showIntroduction;
/// If this is true the introduction will always be shown.
final bool alwaysShowIntroduction;
/// If the killswitch is enabled this app can be remotely disabled. /// If the killswitch is enabled this app can be remotely disabled.
final bool useKillswitch; final bool useKillswitch;
@ -65,10 +64,4 @@ class StartUserStoryConfiguration {
/// The future that is awaited before the splash screen is closed. /// The future that is awaited before the splash screen is closed.
final Future<String?> Function(BuildContext context)? splashScreenFuture; final Future<String?> Function(BuildContext context)? splashScreenFuture;
/// Allow popping from introduction, defaults to true
final bool canPopFromIntroduction;
/// If the splash screen should be shown or not.
final bool showSplashScreen;
} }

View file

@ -3,22 +3,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
/// A service class to check if a killswitch is active for the current app. class KillswitchService {
abstract interface class KillswitchService {
/// Checks if the killswitch is active for the current app.
///
/// It makes a GET request to a specific URL with the app
/// name as a query parameter.
/// If the request fails or times out after 5 seconds,
/// it defaults to returning 'false'.
///
/// Returns a [Future] that completes with 'true' if the killswitch is active,
/// and 'false' otherwise.
Future<bool> isKillswitchActive() => throw UnimplementedError();
}
class DefaultKillswitchService implements KillswitchService {
@override
Future<bool> isKillswitchActive() async { Future<bool> isKillswitchActive() async {
var packageInfo = await PackageInfo.fromPlatform(); var packageInfo = await PackageInfo.fromPlatform();
var appName = packageInfo.appName; var appName = packageInfo.appName;

View file

@ -11,19 +11,19 @@ import 'package:flutter_start/src/go_router.dart';
import 'package:flutter_start/src/models/start_configuration.dart'; import 'package:flutter_start/src/models/start_configuration.dart';
import 'package:flutter_start/src/routes.dart'; import 'package:flutter_start/src/routes.dart';
import 'package:flutter_start/src/services/killswitch_service.dart'; import 'package:flutter_start/src/services/killswitch_service.dart';
import 'package:flutter_start/src/widgets/default_splash_screen.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
List<GoRoute> getStartStoryRoutes({ List<GoRoute> getStartStoryRoutes(
StartUserStoryConfiguration? configuration = StartUserStoryConfiguration configuration,
const StartUserStoryConfiguration(), ) =>
}) =>
<GoRoute>[ <GoRoute>[
GoRoute( GoRoute(
path: StartUserStoryRoutes.splashScreen, path: StartUserStoryRoutes.splashScreen,
pageBuilder: (context, state) { pageBuilder: (context, state) {
var go = context.go; var go = context.go;
var isAllowedToPassThrough = false; var killSwitchIsActive = false;
var introductionSeen = false;
String? routeAfterSplash; String? routeAfterSplash;
Future<void> splashLoadingMethod() async { Future<void> splashLoadingMethod() async {
await Future.wait<void>( await Future.wait<void>(
@ -31,14 +31,14 @@ List<GoRoute> getStartStoryRoutes({
Future.delayed( Future.delayed(
Duration.zero, Duration.zero,
() async { () async {
if (configuration!.useKillswitch) { if (configuration.useKillswitch)
var killswitchService = configuration.killswitchService ?? killSwitchIsActive =
DefaultKillswitchService(); await KillswitchService().isKillswitchActive();
var introService = configuration.introductionService ??
isAllowedToPassThrough = IntroductionService(
await killswitchService.isKillswitchActive(); SharedPreferencesIntroductionDataProvider(),
} );
introductionSeen = !await introService.shouldShow();
if (context.mounted) if (context.mounted)
routeAfterSplash = await configuration.splashScreenFuture routeAfterSplash = await configuration.splashScreenFuture
?.call(context) ?? ?.call(context) ??
@ -47,16 +47,17 @@ List<GoRoute> getStartStoryRoutes({
), ),
Future.delayed( Future.delayed(
Duration( Duration(
seconds: configuration!.minimumSplashScreenDuration, seconds: configuration.minimumSplashScreenDuration,
), ),
() async {}, () async {},
), ),
], ],
); );
if (configuration.useKillswitch && isAllowedToPassThrough) return; if (configuration.useKillswitch && killSwitchIsActive) return;
if ((!configuration.showIntroduction) && context.mounted) { if (!configuration.showIntroduction ||
(introductionSeen && !configuration.alwaysShowIntroduction)) {
return go( return go(
routeAfterSplash ?? StartUserStoryRoutes.home, routeAfterSplash ?? StartUserStoryRoutes.home,
); );
@ -64,7 +65,7 @@ List<GoRoute> getStartStoryRoutes({
return go(StartUserStoryRoutes.introduction); return go(StartUserStoryRoutes.introduction);
} }
if (configuration!.splashScreenBuilder == null) { if (configuration.splashScreenBuilder == null) {
unawaited(splashLoadingMethod()); unawaited(splashLoadingMethod());
} }
return buildScreenWithoutTransition( return buildScreenWithoutTransition(
@ -79,7 +80,7 @@ List<GoRoute> getStartStoryRoutes({
body: Center( body: Center(
child: child:
configuration.splashScreenCenterWidget?.call(context) ?? configuration.splashScreenCenterWidget?.call(context) ??
defaultSplashScreen(context), const SizedBox.shrink(),
), ),
), ),
); );
@ -89,7 +90,7 @@ List<GoRoute> getStartStoryRoutes({
path: StartUserStoryRoutes.introduction, path: StartUserStoryRoutes.introduction,
pageBuilder: (context, state) { pageBuilder: (context, state) {
var introduction = Introduction( var introduction = Introduction(
service: configuration!.introductionService ?? service: configuration.introductionService ??
IntroductionService( IntroductionService(
SharedPreferencesIntroductionDataProvider(), SharedPreferencesIntroductionDataProvider(),
), ),
@ -103,12 +104,9 @@ List<GoRoute> getStartStoryRoutes({
physics: configuration.introductionScrollPhysics, physics: configuration.introductionScrollPhysics,
child: configuration.introductionFallbackScreen, child: configuration.introductionFallbackScreen,
); );
return buildScreenWithoutTransition( return buildScreenWithoutTransition(
context: context, context: context,
state: state, state: state,
child: PopScope(
canPop: configuration.canPopFromIntroduction,
child: configuration.introductionBuilder?.call( child: configuration.introductionBuilder?.call(
context, context,
introduction, introduction,
@ -116,7 +114,6 @@ List<GoRoute> getStartStoryRoutes({
Scaffold( Scaffold(
body: introduction, body: introduction,
), ),
),
); );
}, },
), ),

View file

@ -1,172 +1,110 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_start/flutter_start.dart'; import 'package:flutter_start/flutter_start.dart';
import 'package:flutter_start/src/services/killswitch_service.dart'; import 'package:flutter_start/src/services/killswitch_service.dart';
import 'package:flutter_start/src/widgets/default_splash_screen.dart';
/// Initial screen of the user story. Widget startNavigatorUserStory(
///
/// Use this when defining an initial route.
class NavigatorStartUserStory extends StatelessWidget {
const NavigatorStartUserStory({
required this.onComplete,
this.configuration = const StartUserStoryConfiguration(),
super.key,
});
final StartUserStoryConfiguration configuration;
final void Function(BuildContext context) onComplete;
@override
Widget build(BuildContext context) => Navigator(
onGenerateInitialRoutes: (_, __) => [
_getInitialRoute(configuration, onComplete),
],
);
}
/// Enter the start user story with the Navigator 1.0 API.
///
/// Requires a Navigator widget to exist in the given [context].
///
/// Customization can be done through the [configuration] parameter.
///
/// [onComplete] triggers as soon as the userstory is finished.
///
/// The context provided here is a context has a guaranteed navigator.
Future<void> startNavigatorUserStory(
BuildContext context,
StartUserStoryConfiguration configuration, {
required void Function(BuildContext context) onComplete,
}) async {
var initialRoute = _getInitialRoute(configuration, onComplete);
await Navigator.of(context).push(initialRoute);
}
MaterialPageRoute<dynamic> _getInitialRoute(
StartUserStoryConfiguration configuration, StartUserStoryConfiguration configuration,
void Function(BuildContext context) onComplete, BuildContext context,
) { ) {
var initialRoute = MaterialPageRoute( if (configuration.splashScreenBuilder == null &&
builder: (context) => _splashScreen( configuration.splashScreenCenterWidget == null &&
configuration, configuration.splashScreenBackgroundColor == null) {
context, return _introduction(configuration, context);
onComplete,
),
);
if (!configuration.showSplashScreen && configuration.showIntroduction) {
initialRoute = MaterialPageRoute(
builder: (context) => _introduction(
configuration,
context,
onComplete,
),
);
} }
return initialRoute; return _splashScreen(configuration, context);
} }
Widget _splashScreen( Widget _splashScreen(
StartUserStoryConfiguration configuration, StartUserStoryConfiguration configuration,
BuildContext context, BuildContext context,
void Function(BuildContext context) onComplete,
) { ) {
var navigator = Navigator.of(context); var navigator = Navigator.of(context);
var killSwitchIsActive = false;
var isAllowedToPassThrough = false; var introductionSeen = false;
Future<void> myFunction() async {
Future<void> splashHandler() async {
await Future.wait<void>( await Future.wait<void>(
[ [
configuration.splashScreenFuture?.call(context) ?? Future.value(), configuration.splashScreenFuture?.call(context) ?? Future.value(),
Future.delayed( Future.delayed(
Duration.zero, Duration.zero,
() async { () async {
if (configuration.useKillswitch) { if (configuration.useKillswitch)
var killswitchService = killSwitchIsActive =
configuration.killswitchService ?? DefaultKillswitchService(); await KillswitchService().isKillswitchActive();
var introService = configuration.introductionService ??
isAllowedToPassThrough = IntroductionService(
await killswitchService.isKillswitchActive(); SharedPreferencesIntroductionDataProvider(),
} );
introductionSeen = !await introService.shouldShow();
}, },
), ),
Future.delayed( Future.delayed(
Duration( Duration(
seconds: configuration.minimumSplashScreenDuration, seconds: configuration.minimumSplashScreenDuration,
), ),
() async {},
), ),
], ],
); );
if (configuration.useKillswitch && isAllowedToPassThrough) return; if (configuration.useKillswitch && killSwitchIsActive) return;
var introService = configuration.introductionService ??
IntroductionService(SharedPreferencesIntroductionDataProvider());
var shouldShowIntroduction =
configuration.showIntroduction && await introService.shouldShow();
if (!context.mounted) return;
if (!shouldShowIntroduction) return onComplete(context);
if (!configuration.showIntroduction ||
(introductionSeen && !configuration.alwaysShowIntroduction)) {
await navigator.pushReplacement( await navigator.pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => _introduction( builder: (context) => _home(configuration, context),
configuration,
context,
onComplete,
), ),
);
}
await navigator.pushReplacement(
MaterialPageRoute(
builder: (context) => _introduction(configuration, context),
), ),
); );
} }
unawaited(splashHandler()); return configuration.splashScreenBuilder?.call(
context,
var builder = configuration.splashScreenBuilder; () async => myFunction(),
) ??
if (builder == null) { Scaffold(
return Scaffold(
backgroundColor: configuration.splashScreenBackgroundColor, backgroundColor: configuration.splashScreenBackgroundColor,
body: Center( body: Center(
child: configuration.splashScreenCenterWidget?.call(context) ?? child: configuration.splashScreenCenterWidget?.call(context) ??
defaultSplashScreen(context), const SizedBox.shrink(),
), ),
); );
} }
return builder.call(
context,
splashHandler,
);
}
Widget _introduction( Widget _introduction(
StartUserStoryConfiguration configuration, StartUserStoryConfiguration configuration,
BuildContext context, BuildContext context,
void Function(BuildContext context) onComplete,
) { ) {
var introduction = Introduction( var introduction = Introduction(
service: configuration.introductionService ?? service: configuration.introductionService ??
IntroductionService(SharedPreferencesIntroductionDataProvider()), IntroductionService(SharedPreferencesIntroductionDataProvider()),
navigateTo: () async => onComplete(context), navigateTo: () async => Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => _home(configuration, context),
),
),
options: configuration.introductionOptionsBuilder?.call(context) ?? options: configuration.introductionOptionsBuilder?.call(context) ??
const IntroductionOptions(), const IntroductionOptions(),
physics: configuration.introductionScrollPhysics, physics: configuration.introductionScrollPhysics,
child: configuration.introductionFallbackScreen, child: configuration.introductionFallbackScreen,
); );
return PopScope( return Scaffold(
canPop: configuration.canPopFromIntroduction,
child: configuration.introductionBuilder?.call(
context,
introduction,
) ??
Scaffold(
body: introduction, body: introduction,
), );
}
Widget _home(
StartUserStoryConfiguration configuration,
BuildContext context,
) {
var home = configuration.homeEntry;
return Scaffold(
body: home,
); );
} }

View file

@ -1,6 +0,0 @@
import 'package:flutter/material.dart';
Text defaultSplashScreen(BuildContext context) => Text(
'iconinstagram',
style: Theme.of(context).textTheme.headlineLarge,
);

View file

@ -1,8 +1,7 @@
name: flutter_start name: flutter_start
description: "Flutter_start is a package that allows you to jumpstart your application with a splashScreen, introduction and a home." description: "Flutter_start is a package that allows you to jumpstart your application with a splashScreen, introduction and a home."
version: 4.2.4 publish_to: "none"
version: 2.0.3
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
environment: environment:
sdk: ">=3.2.5 <4.0.0" sdk: ">=3.2.5 <4.0.0"
@ -10,16 +9,20 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
cupertino_icons: ">=1.0.2 <2.0.0" cupertino_icons: ^1.0.2
go_router: ">=14.2.0 <15.0.0" go_router: any
http: ">=1.2.1 <2.0.0" http: any
package_info_plus: ">=8.0.0 <9.0.0" package_info_plus: any
flutter_introduction: flutter_introduction:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub git:
version: ^5.0.0 url: https://github.com/Iconica-Development/flutter_introduction
ref: 2.1.0
path: packages/flutter_introduction
flutter_introduction_shared_preferences: flutter_introduction_shared_preferences:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ git:
version: ^5.0.0 url: https://github.com/Iconica-Development/flutter_introduction
ref: 2.1.0
path: packages/flutter_introduction_shared_preferences
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -1,14 +0,0 @@
// This is an example unit test.
//
// A unit test tests a single function, method, or class. To learn more about
// writing unit tests, visit
// https://flutter.dev/docs/cookbook/testing/unit/introduction
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Plus Operator', () {
test('should add two numbers together', () {
expect(1 + 1, 2);
});
});
}