Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/android_build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Build Android APK

on:
push:
branches:
- main
- ios-pwa
workflow_dispatch: # Allows manual trigger from GitHub UI

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
cache: 'gradle'

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true

- name: Install Dependencies
run: flutter pub get

- name: Create dart_defines.json
run: echo '${{ secrets.DART_DEFINES_JSON }}' > dart_defines.json

- name: Build APK
run: flutter build apk --release --dart-define-from-file=dart_defines.json

- name: Upload APK Artifact
uses: actions/upload-artifact@v4
with:
name: schedulify-release-apk
path: build/app/outputs/flutter-apk/app-release.apk
retention-days: 7
5 changes: 3 additions & 2 deletions ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '15.0'
platform :ios, '16.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Expand Down Expand Up @@ -41,7 +41,8 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
end
end
end
15 changes: 14 additions & 1 deletion ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
PODS:
- barcode_scan2 (0.0.1):
- Flutter
- SwiftProtobuf (~> 1.33)
- Flutter (1.0.0)
- SwiftProtobuf (1.38.0)

DEPENDENCIES:
- barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`)
- Flutter (from `Flutter`)

SPEC REPOS:
trunk:
- SwiftProtobuf

EXTERNAL SOURCES:
barcode_scan2:
:path: ".symlinks/plugins/barcode_scan2/ios"
Flutter:
:path: Flutter

SPEC CHECKSUMS:
barcode_scan2: 4e4b850b112f4e29017833e4715f36161f987966
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
SwiftProtobuf: d724b5145bfc609d9a49c1e3e3a3dabb07273ffb

PODFILE CHECKSUM: 5b09406f5533f670a98a35b5bf281f156d27c54b
PODFILE CHECKSUM: ba4b7dd263cdb42897a2433345744d9e6a9917e4

COCOAPODS: 1.16.2
27 changes: 24 additions & 3 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
05E86B82B9FAE641430CFEF1 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
Expand Down Expand Up @@ -282,6 +283,23 @@
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
05E86B82B9FAE641430CFEF1 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
Expand Down Expand Up @@ -443,6 +461,7 @@
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
Expand All @@ -451,7 +470,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down Expand Up @@ -566,6 +585,7 @@
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
Expand All @@ -580,7 +600,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -623,6 +643,7 @@
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
Expand All @@ -631,7 +652,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down
162 changes: 162 additions & 0 deletions lib/core/utils/pwa_prompt.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

// Conditional import: only the web variant accesses dart:js_interop.
import 'pwa_prompt_stub.dart'
if (dart.library.js_interop) 'pwa_prompt_web.dart';

/// Shows an "Add to Home Screen" bottom sheet after login,
/// but ONLY when running as a web app on iOS Safari (not yet installed as PWA).
///
/// Usage: call [PwaPrompt.showIfNeeded] after navigating post-login.
class PwaPrompt {
PwaPrompt._();

static bool _alreadyShown = false;

static Future<void> showIfNeeded(BuildContext context) async {
if (!kIsWeb) return;
if (_alreadyShown) return;
if (!PwaDetector.shouldShow()) return;

_alreadyShown = true;
await Future.delayed(const Duration(milliseconds: 700));
if (!context.mounted) return;

await showModalBottomSheet<void>(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (_) => const _InstallHintSheet(),
);
}
}

class _InstallHintSheet extends StatelessWidget {
const _InstallHintSheet();

@override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
decoration: BoxDecoration(
color: const Color(0xFF1C1C2E),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white.withAlpha(20)),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Drag handle
Container(
width: 40, height: 4,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(2),
),
),
// Header row
Row(children: [
Container(
width: 48, height: 48,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6C63FF), Color(0xFF3B82F6)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.schedule_rounded,
color: Colors.white, size: 26),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Install Schedulify',
style: TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.w700)),
SizedBox(height: 2),
Text('Get the full app experience on iOS',
style: TextStyle(color: Colors.white54, fontSize: 13)),
],
),
),
]),
const SizedBox(height: 20),
const _StepRow(
icon: Icons.ios_share_rounded,
text: 'Tap the Share button',
sub: 'at the bottom of your browser bar',
),
const SizedBox(height: 12),
const _StepRow(
icon: Icons.add_box_outlined,
text: 'Tap "Add to Home Screen"',
sub: 'then tap Add in the top right',
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: TextButton(
onPressed: () => Navigator.of(context).pop(),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
backgroundColor: Colors.white.withAlpha(15),
),
child: const Text('Maybe later',
style: TextStyle(color: Colors.white70, fontSize: 15)),
),
),
],
),
),
),
);
}
}

class _StepRow extends StatelessWidget {
final IconData icon;
final String text;
final String sub;
const _StepRow(
{required this.icon, required this.text, required this.sub});

@override
Widget build(BuildContext context) {
return Row(children: [
Container(
width: 40, height: 40,
decoration: BoxDecoration(
color: Colors.white.withAlpha(13),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: Colors.white70, size: 20),
),
const SizedBox(width: 14),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(text,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600)),
Text(sub,
style: const TextStyle(color: Colors.white38, fontSize: 12)),
],
),
]);
}
}
6 changes: 6 additions & 0 deletions lib/core/utils/pwa_prompt_stub.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// Stub used on non-web platforms.
/// On native (Android/iOS) this class always returns false,
/// so the PWA prompt is never shown.
class PwaDetector {
static bool shouldShow() => false;
}
15 changes: 15 additions & 0 deletions lib/core/utils/pwa_prompt_web.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'dart:js_interop';

@JS('window._schedulifyShowPwaPrompt')
external JSBoolean? get _showPwaPrompt;

/// Web implementation: reads the flag set by the inline JS in index.html.
class PwaDetector {
static bool shouldShow() {
try {
return _showPwaPrompt?.toDart ?? false;
} catch (_) {
return false;
}
}
}
3 changes: 3 additions & 0 deletions lib/features/auth/screens/login_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/providers/auth_provider.dart';
import '../../../core/utils/pwa_prompt.dart';
import '../../../shared/widgets/widgets.dart';

class LoginScreen extends ConsumerStatefulWidget {
Expand Down Expand Up @@ -42,6 +43,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
_ => '/student',
};
context.go(route);
// Show iOS PWA install hint (no-op on Android native & desktop).
await PwaPrompt.showIfNeeded(context);
}
}

Expand Down
Loading
Loading