diff --git a/app/(application)/(user)/job/[id]/[status]/detail.tsx b/app/(application)/(user)/job/[id]/[status]/detail.tsx
index 12bd4c4..9564cab 100644
--- a/app/(application)/(user)/job/[id]/[status]/detail.tsx
+++ b/app/(application)/(user)/job/[id]/[status]/detail.tsx
@@ -5,7 +5,7 @@ import {
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
- NewWrapper_V2,
+ PageWrapper,
Spacing,
StackCustom,
} from "@/components";
@@ -72,7 +72,7 @@ export default function JobDetailStatus() {
),
}}
/>
-
+
{isLoadData ? (
) : (
@@ -96,7 +96,7 @@ export default function JobDetailStatus() {
>
)}
-
+
) : (
-
+
<>
@@ -83,17 +83,10 @@ export default function JobDetailArchive() {
>
Publish kembali
- {/* */}
>
-
+
)}
>
);
diff --git a/app/(application)/(user)/job/[id]/index.tsx b/app/(application)/(user)/job/[id]/index.tsx
index a4183a1..c00e257 100644
--- a/app/(application)/(user)/job/[id]/index.tsx
+++ b/app/(application)/(user)/job/[id]/index.tsx
@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
-import { ButtonCustom, LoaderCustom, Spacing, StackCustom, ViewWrapper } from "@/components";
+import { ButtonCustom, LoaderCustom, PageWrapper, Spacing, StackCustom } from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection";
@@ -88,7 +88,7 @@ export default function JobDetail() {
};
return (
-
+
{isLoading ? (
) : (
@@ -101,6 +101,6 @@ export default function JobDetail() {
>
)}
-
+
);
}
diff --git a/components/_ShareComponent/PageWrapper.tsx b/components/_ShareComponent/PageWrapper.tsx
new file mode 100644
index 0000000..f75f1aa
--- /dev/null
+++ b/components/_ShareComponent/PageWrapper.tsx
@@ -0,0 +1,229 @@
+/**
+ * PageWrapper - Platform-specific wrapper component
+ *
+ * Routes to:
+ * - iOS: NewWrapper (stable, tested)
+ * - Android: NewWrapper_V2 (with keyboard handling fix)
+ *
+ * Props are automatically adjusted based on platform.
+ *
+ * @example
+ *
+ * {children}
+ *
+ */
+
+import { Platform } from "react-native";
+import { NewWrapper_V2 } from "./NewWrapper_V2";
+import type { NativeSafeAreaViewProps } from "react-native-safe-area-context";
+import type { ScrollViewProps, FlatListProps } from "react-native";
+import NewWrapper from "./NewWrapper";
+
+// ========== Base Props ==========
+interface BaseProps {
+ withBackground?: boolean;
+ headerComponent?: React.ReactNode;
+ footerComponent?: React.ReactNode;
+ floatingButton?: React.ReactNode;
+ hideFooter?: boolean;
+ edgesFooter?: NativeSafeAreaViewProps["edges"];
+ style?: any;
+ refreshControl?: ScrollViewProps["refreshControl"];
+}
+
+// ========== Static Mode Props ==========
+interface StaticModeProps extends BaseProps {
+ children: React.ReactNode;
+ listData?: never;
+ renderItem?: never;
+}
+
+// ========== List Mode Props ==========
+interface ListModeProps extends BaseProps {
+ children?: never;
+ listData?: any[];
+ renderItem?: FlatListProps["renderItem"];
+ onEndReached?: () => void;
+ ListHeaderComponent?: React.ReactElement | null;
+ ListFooterComponent?: React.ReactElement | null;
+ ListEmptyComponent?: React.ReactElement | null;
+ keyExtractor?: FlatListProps["keyExtractor"];
+}
+
+// ========== PageWrapper Props ==========
+interface PageWrapperBaseProps extends BaseProps {
+ /**
+ * Enable keyboard handling (Android only - NewWrapper_V2)
+ * iOS ignores this prop
+ * @default false
+ */
+ enableKeyboardHandling?: boolean;
+
+ /**
+ * Scroll offset when keyboard appears (Android only)
+ * iOS ignores this prop
+ * @default 100
+ */
+ keyboardScrollOffset?: number;
+
+ /**
+ * Extra padding bottom for content (Android only)
+ * iOS ignores this prop
+ * @default 80
+ */
+ contentPaddingBottom?: number;
+
+ /**
+ * Padding Top for content container (Android only)
+ * iOS ignores this prop
+ * @default 8
+ */
+ contentPaddingTop?: number;
+
+ /**
+ * Padding Horizontal for content container (Android only)
+ * iOS ignores this prop
+ * @default 0
+ */
+ contentPaddingHorizontal?: number;
+}
+
+interface PageWrapperStaticProps extends PageWrapperBaseProps {
+ children: React.ReactNode;
+ listData?: never;
+ renderItem?: never;
+}
+
+interface PageWrapperListProps extends PageWrapperBaseProps {
+ children?: never;
+ listData?: any[];
+ renderItem?: FlatListProps["renderItem"];
+ onEndReached?: () => void;
+ ListHeaderComponent?: React.ReactElement | null;
+ ListFooterComponent?: React.ReactElement | null;
+ ListEmptyComponent?: React.ReactElement | null;
+ keyExtractor?: FlatListProps["keyExtractor"];
+}
+
+type PageWrapperProps = PageWrapperStaticProps | PageWrapperListProps;
+
+export function PageWrapper(props: PageWrapperProps) {
+ const {
+ withBackground,
+ headerComponent,
+ footerComponent,
+ floatingButton,
+ hideFooter,
+ edgesFooter,
+ style,
+ refreshControl,
+ enableKeyboardHandling,
+ keyboardScrollOffset,
+ contentPaddingBottom,
+ contentPaddingTop,
+ contentPaddingHorizontal,
+ ...restProps
+ } = props;
+
+ // ========== Android: Use NewWrapper_V2 with keyboard handling ==========
+ if (Platform.OS === "android") {
+ if ("listData" in props) {
+ // List mode
+ const listProps = props as PageWrapperListProps;
+ return (
+
+ );
+ }
+
+ // Static mode
+ const staticProps = props as PageWrapperStaticProps;
+ return (
+
+ {staticProps.children}
+
+ );
+ }
+
+ // ========== iOS: Use NewWrapper (stable) ==========
+ if ("listData" in props) {
+ // List mode
+ const listProps = props as PageWrapperListProps;
+ return (
+
+ );
+ }
+
+ // Static mode
+ const staticProps = props as PageWrapperStaticProps;
+ return (
+
+ {staticProps.children}
+
+ );
+}
+
+export default PageWrapper;
diff --git a/components/index.ts b/components/index.ts
index 6b7f156..9d850ee 100644
--- a/components/index.ts
+++ b/components/index.ts
@@ -65,6 +65,7 @@ import NewWrapper from "./_ShareComponent/NewWrapper";
import BasicWrapper from "./_ShareComponent/BasicWrapper";
import { FormWrapper } from "./_ShareComponent/FormWrapper";
import { NewWrapper_V2 } from "./_ShareComponent/NewWrapper_V2";
+import { PageWrapper } from "./_ShareComponent/PageWrapper";
// Progress
import ProgressCustom from "./Progress/ProgressCustom";
@@ -132,6 +133,7 @@ export {
BasicWrapper,
FormWrapper,
NewWrapper_V2,
+ PageWrapper,
// Stack
StackCustom,
TabBarBackground,
diff --git a/docs/PAGEWRAPPER-USAGE.md b/docs/PAGEWRAPPER-USAGE.md
new file mode 100644
index 0000000..dc75abc
--- /dev/null
+++ b/docs/PAGEWRAPPER-USAGE.md
@@ -0,0 +1,304 @@
+# PageWrapper - Platform-Specific Wrapper
+
+## 📋 Overview
+
+`PageWrapper` adalah wrapper component yang secara otomatis memilih wrapper yang tepat berdasarkan platform:
+
+- **iOS**: Menggunakan `NewWrapper` (stable, tested)
+- **Android**: Menggunakan `NewWrapper_V2` (dengan keyboard handling fix)
+
+## 🎯 Kapan Menggunakan PageWrapper?
+
+### ✅ **Gunakan PageWrapper untuk:**
+- Screen baru yang kamu buat
+- Migrasi screen existing dari `NewWrapper`/`ViewWrapper`
+- Form screens dengan TextInput/TextArea
+- List screens dengan pagination
+
+### ❌ **Jangan gunakan PageWrapper untuk:**
+- Screen yang sudah menggunakan `NewWrapper_V2` langsung dan sudah tested di iOS
+- Custom wrapper requirements
+
+---
+
+## 📝 Usage
+
+### **Basic Usage (Static Content)**
+
+```typescript
+import { PageWrapper } from "@/components";
+
+export function MyScreen() {
+ return (
+ }
+ >
+
+
+
+
+
+ );
+}
+```
+
+### **With Keyboard Handling (Android Only)**
+
+```typescript
+}
+>
+
+ true}>
+
+
+ true}>
+
+
+
+
+```
+
+### **List Mode (Pagination)**
+
+```typescript
+
+ }
+/>
+```
+
+---
+
+## 🔧 Props
+
+### **Common Props (iOS & Android)**
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `footerComponent` | `ReactNode` | - | Fixed footer component |
+| `headerComponent` | `ReactNode` | - | Header component (sticky) |
+| `floatingButton` | `ReactNode` | - | Floating button overlay |
+| `hideFooter` | `boolean` | `false` | Hide footer footer |
+| `withBackground` | `boolean` | `false` | Use background image |
+| `style` | `ViewStyle` | - | Custom container style |
+| `refreshControl` | `RefreshControl` | - | Pull-to-refresh control |
+
+### **Android-Only Props** (Ignored di iOS)
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `enableKeyboardHandling` | `boolean` | `false` | Enable keyboard auto-scroll |
+| `keyboardScrollOffset` | `number` | `100` | Scroll offset when keyboard appears |
+| `contentPaddingBottom` | `number` | `80` | Bottom padding for content |
+| `contentPaddingTop` | `number` | `8` | Top padding for content |
+| `contentPaddingHorizontal` | `number` | `0` | Horizontal padding for content |
+
+---
+
+## 📊 Platform Behavior
+
+| Feature | iOS (NewWrapper) | Android (NewWrapper_V2) |
+|---------|------------------|------------------------|
+| **Keyboard Handling** | ❌ No auto-scroll | ✅ Auto-scroll to input |
+| **Footer Position** | ✅ Fixed bottom | ✅ Fixed bottom (absolute) |
+| **Safe Area** | ✅ Handled | ✅ Handled |
+| **Content Padding** | Default | Customizable |
+| **List Mode** | ✅ Supported | ✅ Supported |
+
+---
+
+## 🔄 Migration Guide
+
+### **From NewWrapper**
+
+```typescript
+// BEFORE
+import { NewWrapper } from "@/components";
+
+
+ {children}
+
+
+// AFTER
+import { PageWrapper } from "@/components";
+
+
+ {children}
+
+```
+
+### **From NewWrapper_V2**
+
+```typescript
+// BEFORE
+import { NewWrapper_V2 } from "@/components";
+
+
+ true}>
+
+
+
+
+// AFTER
+import { PageWrapper } from "@/components";
+
+
+ true}>
+
+
+
+```
+
+---
+
+## ⚠️ Important Notes
+
+### **For Form Screens (Android)**
+
+Jika menggunakan `enableKeyboardHandling`, **WAJIB wrap semua input** dengan `View onStartShouldSetResponder`:
+
+```typescript
+ true}>
+
+
+```
+
+**Kenapa?**
+- Mencegah keyboard handling conflict
+- Memastikan tap outside dismiss keyboard
+- Konsisten behavior di Android
+
+### **For iOS Users**
+
+Props berikut **diabaikan di iOS**:
+- `enableKeyboardHandling`
+- `keyboardScrollOffset`
+- `contentPaddingBottom`
+- `contentPaddingTop`
+- `contentPaddingHorizontal`
+
+iOS menggunakan `NewWrapper` yang sudah stable tanpa keyboard handling.
+
+---
+
+## 🎨 Examples
+
+### **Example 1: Simple Form**
+
+```typescript
+import { PageWrapper, TextInputCustom, StackCustom } from "@/components";
+
+export function SimpleForm() {
+ return (
+ }
+ >
+
+ true}>
+
+
+ true}>
+
+
+
+
+ );
+}
+```
+
+### **Example 2: List with Pagination**
+
+```typescript
+import { PageWrapper } from "@/components";
+
+export function UserList() {
+ const pagination = usePagination({ fetchFunction: fetchUsers });
+
+ return (
+ }
+ onEndReached={pagination.loadMore}
+ ListEmptyComponent={}
+ ListFooterComponent={}
+ refreshControl={
+
+ }
+ />
+ );
+}
+```
+
+### **Example 3: Detail Screen (No Footer)**
+
+```typescript
+import { PageWrapper } from "@/components";
+
+export function DetailScreen() {
+ return (
+
+
+ Title
+ Description
+
+
+ );
+}
+```
+
+---
+
+## 🚀 Future Plans
+
+### **Phase 1: Current** (Now)
+- ✅ `PageWrapper` created
+- ✅ iOS → `NewWrapper` (stable)
+- ✅ Android → `NewWrapper_V2` (keyboard fix)
+
+### **Phase 2: iOS Migration** (1-2 months)
+- [ ] Fix iOS bugs di `NewWrapper_V2`
+- [ ] Test `NewWrapper_V2` di iOS devices
+- [ ] Update `PageWrapper` untuk use V2 untuk iOS juga
+
+### **Phase 3: Unify** (3 months)
+- [ ] Deprecate `NewWrapper` lama
+- [ ] Rename `NewWrapper_V2` → `NewWrapper`
+- [ ] Update `PageWrapper` untuk always use V2
+
+---
+
+## 📚 Related Files
+
+- `components/_ShareComponent/PageWrapper.tsx` - Main component
+- `components/_ShareComponent/NewWrapper.tsx` - iOS wrapper
+- `components/_ShareComponent/NewWrapper_V2.tsx` - Android wrapper
+- `hooks/useKeyboardForm.ts` - Keyboard handling hook
+
+---
+
+**Last Updated**: 2026-04-06
+**Created by**: AI Assistant
+**Status**: ✅ Ready to use
diff --git a/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj b/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
index d2032ce..9a03aa5 100644
--- a/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
+++ b/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
@@ -156,6 +156,7 @@
92A25C61F4E34FB6A36E415B /* Remove signature files (Xcode workaround) */,
6440E59133324659A2C60D0B /* Remove signature files (Xcode workaround) */,
35CC0495598542E6801662A3 /* Remove signature files (Xcode workaround) */,
+ 5ED53AFC8AD1445DA81C7BD4 /* Remove signature files (Xcode workaround) */,
);
buildRules = (
);
@@ -501,6 +502,23 @@
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
+ 5ED53AFC8AD1445DA81C7BD4 /* Remove signature files (Xcode workaround) */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ name = "Remove signature files (Xcode workaround)";
+ inputPaths = (
+ );
+ outputPaths = (
+ );
+ shellPath = /bin/sh;
+ shellScript = "
+ echo \"Remove signature files (Xcode workaround)\";
+ rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
+ ";
+ };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
diff --git a/screens/Job/MainViewStatus2.tsx b/screens/Job/MainViewStatus2.tsx
index b8951f2..1e9857c 100644
--- a/screens/Job/MainViewStatus2.tsx
+++ b/screens/Job/MainViewStatus2.tsx
@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
-import { BaseBox, NewWrapper_V2, ScrollableCustom, TextCustom } from "@/components";
+import { BaseBox, PageWrapper, ScrollableCustom, TextCustom } from "@/components";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
@@ -86,7 +86,7 @@ export default function Job_MainViewStatus2() {
);
return (
- {scrollComponent}}
listData={pagination.listData}
diff --git a/screens/Job/ScreenArchive.tsx b/screens/Job/ScreenArchive.tsx
index f9d93fd..94bd677 100644
--- a/screens/Job/ScreenArchive.tsx
+++ b/screens/Job/ScreenArchive.tsx
@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
-import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components";
+import { BaseBox, LoaderCustom, PageWrapper, TextCustom } from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiJobGetAll } from "@/service/api-client/api-job";
import { useFocusEffect } from "expo-router";
@@ -33,7 +33,7 @@ export default function Job_ScreenArchive() {
};
return (
-
+
{isLoadData ? (
) : _.isEmpty(listData) ? (
@@ -52,6 +52,6 @@ export default function Job_ScreenArchive() {
))
)}
-
+
);
}
diff --git a/screens/Job/ScreenArchive2.tsx b/screens/Job/ScreenArchive2.tsx
index 7bcbb30..3d2e98b 100644
--- a/screens/Job/ScreenArchive2.tsx
+++ b/screens/Job/ScreenArchive2.tsx
@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
-import { BaseBox, NewWrapper_V2, TextCustom, ViewWrapper } from "@/components";
+import { BaseBox, PageWrapper, TextCustom } from "@/components";
import { MainColor } from "@/constants/color-palet";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
@@ -55,7 +55,7 @@ export default function Job_ScreenArchive2() {
);
return (
-
+
setSearch(text), 500)}
diff --git a/screens/Job/ScreenJobCreate.tsx b/screens/Job/ScreenJobCreate.tsx
index 44866eb..e2eb09b 100644
--- a/screens/Job/ScreenJobCreate.tsx
+++ b/screens/Job/ScreenJobCreate.tsx
@@ -4,7 +4,7 @@ import {
ButtonCustom,
InformationBox,
LandscapeFrameUploaded,
- NewWrapper_V2,
+ PageWrapper,
Spacing,
StackCustom,
TextAreaCustom,
@@ -118,7 +118,7 @@ export function Job_ScreenCreate() {
};
return (
-
-
+
);
}
diff --git a/screens/Job/ScreenJobEdit.tsx b/screens/Job/ScreenJobEdit.tsx
index 344c6bb..6a8fd14 100644
--- a/screens/Job/ScreenJobEdit.tsx
+++ b/screens/Job/ScreenJobEdit.tsx
@@ -8,7 +8,7 @@ import {
InformationBox,
LandscapeFrameUploaded,
LoaderCustom,
- NewWrapper_V2,
+ PageWrapper,
Spacing,
StackCustom,
TextAreaCustom,
@@ -134,7 +134,7 @@ export function Job_ScreenEdit() {
};
return (
-
)}
-
+
);
}